C/C++教程

【算法】一文刷完LeetCode全部典型题(持续更新)

本文主要是介绍【算法】一文刷完LeetCode全部典型题(持续更新),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1 题型分类

2 贪心算法

2.1 算法概念

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择
贪心算法一般按如下步骤进行:
①建立数学模型来描述问题。
②把求解的问题分成若干个子问题。
③对每个子问题求解,得到子问题的局部最优解。
④把子问题的解局部最优解合成原来解问题的一个解。

2.2 分配问题

  1. 分发饼干
    假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
    对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
    示例 1:
    输入: g = [1,2,3], s = [1,1]
    输出: 1
    解释:
    你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
    虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
    所以你应该输出1。
    示例 2:
    输入: g = [1,2], s = [1,2,3]
    输出: 2
    解释:
    你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
    你拥有的饼干数量和尺寸都足以让所有孩子满足。
    所以你应该输出2.

题解:
优先满足胃口最小的孩子,把大于等于这个孩子饥饿度g[i],且最小的饼干s[j]分配给这个孩子。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int i = 0, j = 0;
        while (i < g.length && j < s.length) {
            if (g[i] <= s[j]) ++i;
            ++j;
        }
        return i;
    }
}

执行用时: 8 ms 内存消耗: 39.2 MB

官方题解:

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int numOfChildren = g.length, numOfCookies = s.length;
        int count = 0;
        for (int i = 0, j = 0; i < numOfChildren && j < numOfCookies; i++, j++) {
            while (j < numOfCookies && g[i] > s[j]) {
                j++;//饼干尺寸小于孩子胃口把饼干序号加1
            }
            if (j < numOfCookies) {
                count++;
            }
        }
        return count;
    }
}

执行用时: 9 ms 内存消耗: 39.4 MB

  1. 分发糖果
    老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
    你需要按照以下要求,帮助老师给这些孩子分发糖果:
    每个孩子至少分配到 1 个糖果。
    评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。
    那么这样下来,老师至少需要准备多少颗糖果呢?
    示例 1:
    输入:[1,0,2]
    输出:5
    解释:你可以分别给这三个孩子分发 2、1、2 颗糖果。

题解:
把所有孩子的糖果数初始化为 1,先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。

class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        if (n < 2) {//只有1个孩子情况直接返回1
            return n;
        }
        int[] num = new int[n];//创建糖果数组初始值默认为0,返回值要加上n
        for (int i = 1; i < n; ++i) {//从左往右遍历
            if (ratings[i] > ratings[i-1]) {//如果右侧孩子评分大于左侧
                num[i] = num[i-1] + 1;//右侧孩子糖果加1
            }
        }
        for (int i = n - 1; i > 0; --i) {//从右往左遍历
            if (ratings[i] < ratings[i-1]) {//如果右侧孩子评分小于左侧
                num[i-1] = Math.max(num[i-1], num[i] + 1);//左侧糖果为原数和右侧加1的较大值
            }
        }
        return Arrays.stream(num).sum() + n;//数组num求和并加上初始糖果数n
    }
}

执行用时: 7 ms 内存消耗: 39.2 MB(在for循环中++i和i++的结果是一样的都在循环一次之后执行,但++i性能更好)

官方题解:

class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        int[] left = new int[n];
        for (int i = 0; i < n; i++) {
            if (i > 0 && ratings[i] > ratings[i - 1]) {
                left[i] = left[i - 1] + 1;
            } else {
                left[i] = 1;
            }
        }
        int right = 0, ret = 0;
        for (int i = n - 1; i >= 0; i--) {
            if (i < n - 1 && ratings[i] > ratings[i + 1]) {
                right++;
            } else {
                right = 1;
            }
            ret += Math.max(left[i], right);
        }
        return ret;
    }
}

执行用时: 3 ms 内存消耗: 39.5 MB

  1. 种花问题
    假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
    给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
    示例 1:
    输入:flowerbed = [1,0,0,0,1], n = 1
    输出:true

题解:
遍历花坛中的元素,满足种花条件则n减1,返回判断n是否<=0

class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        for (int i = 0; i < flowerbed.length; ++i) {
            if (flowerbed[i] == 0 && (i + 1 == flowerbed.length || flowerbed[i + 1] == 0)) {
                n--;
                i++;
            } else if (flowerbed[i] == 1) {
                i++;
            }
        }
        return n <= 0;
    }
}

执行用时: 1 ms 内存消耗: 40 MB

官方题解:
贪心策略:在不打破种植规则的情况下种入尽可能多的花,然后判断可以种入的花的最多数量是否大于或等于 n。
实现方法:
维护 prev 表示上一朵已经种植的花的下标位置,初始时prev=−1,表示尚未遇到任何已经种植的花。
从左往右遍历数组flowerbed,当遇到flowerbed[i]=1时根据previ 的值计算上一个区间内可以种植花的最多数量,然后令prev=i,继续遍历数组 flowerbed 剩下的元素。
遍历数组flowerbed 结束后,根据数组prev 和长度 m 的值计算最后一个区间内可以种植花的最多数量。
判断整个花坛内可以种入的花的最多数量是否大于或等于 n

class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int count = 0;
        int m = flowerbed.length;
        int prev = -1;
        for (int i = 0; i < m; i++) {
            if (flowerbed[i] == 1) {
                if (prev < 0) {
                    count += i / 2;
                } else {
                    count += (i - prev - 2) / 2;
                }
                prev = i;
            }
        }
        if (prev < 0) {
            count += (m + 1) / 2;
        } else {
            count += (m - prev - 1) / 2;
        }
        return count >= n;
    }
}

执行用时: 1 ms 内存消耗: 39.8 MB

2.3 区间问题

  1. 无重叠区间
    给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
    注意:
    可以认为区间的终点总是大于它的起点。
    区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
    示例 1:
    输入: [ [1,2], [2,3], [3,4], [1,3] ]
    输出: 1
    解释: 移除 [1,3] 后,剩下的区间没有重叠。

题解:
贪心策略:优先保留结尾小且不相交的区间。
实现方法:先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals.length == 0) {
            return 0;
        }
        Arrays.sort(intervals, new Comparator<int[]>() {//将数组集合按第二个元素大小排序
            public int compare(int[] interval1, int[] interval2) {
                return interval1[1] - interval2[1];
            }
        });
        int n = intervals.length;
        int total = 0, prev = intervals[0][1];//预置为数组中的第二个元素
        for (int i = 1; i < n; ++i) {
            if (intervals[i][0] < prev) {//该数组的第一个元素与前一个数组的第二元素进行比较
                ++total;//如果prev大,区间有重叠,移除该区间
            } else {
                prev = intervals[i][1];//将prev置为第i个数组的第二个元素
            }
        }
    return total;
    }
}

执行用时: 3 ms 内存消耗: 38.1 MB

官方题解:

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals.length == 0) {
            return 0;
        }
        
        Arrays.sort(intervals, new Comparator<int[]>() {
            public int compare(int[] interval1, int[] interval2) {
                return interval1[1] - interval2[1];
            }
        });

        int n = intervals.length;
        int right = intervals[0][1];
        int ans = 1;
        for (int i = 1; i < n; ++i) {
            if (intervals[i][0] >= right) {
                ++ans;
                right = intervals[i][1];
            }
        }
        return n - ans;
    }
}

执行用时: 3 ms 内存消耗: 38.1 MB

  1. 用最少数量的箭引爆气球
    在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
    一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
    给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
    示例 1:
    输入:points = [[10,16],[2,8],[1,6],[7,12]]
    输出:2
    解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球

题解:
贪心策略:相当于435题的反面,尽量使区间重叠,注意区间可为负数
实现方法:先把区间按照结尾的大小进行增序排序 (区间可以为负数,注意此处排序方式),每次选择结尾最小且和前一个选择的区间重叠的区间。

class Solution {
    public int findMinArrowShots(int[][] points) {
        if (points.length == 0) {
            return 0;
        }
        Arrays.sort(points, new Comparator<int[]> () {
            public int compare (int[] point1, int[] point2) {
                return point1[1] > point2[1] ? 1 : -1;
            }
        });
        int n = points.length;
        int total = n, prev = points[0][1];
        for (int i = 1; i < n; ++i) {
            if (points[i][0] <= prev) {
                --total;
            } else {
                prev = points[i][1];
            }
        }
        return total;
    }
}

执行用时: 19 ms 内存消耗: 45.4 MB

官方题解:

class Solution {
    public int findMinArrowShots(int[][] points) {
        if (points.length == 0) {
            return 0;
        }
        Arrays.sort(points, new Comparator<int[]>() {
            public int compare(int[] point1, int[] point2) {
                if (point1[1] > point2[1]) {
                    return 1;
                } else if (point1[1] < point2[1]) {
                    return -1;
                } else {
                    return 0;
                }
            }
        });
        int pos = points[0][1];
        int ans = 1;
        for (int[] balloon: points) {
            if (balloon[0] > pos) {
                pos = balloon[1];
                ++ans;
            }
        }
        return ans;
    }
}

执行用时: 18 ms 内存消耗: 45.7 MB

3 双指针

3.1 算法概念

双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。

3.2 两数求和

  1. 两数之和 II - 输入有序数组
    给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
    函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。
    你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
    示例 1:
    输入:numbers = [2,7,11,15], target = 9
    输出:[1,2]
    解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2.

题解:
采用方向相反的双指针来寻找这两个数字,一个初始指向最小的元素l,即数组最左边,向右遍历;一个初始指向最大的元素r,即数组最右边,向左遍历。

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int l = 0, r = numbers.length - 1, sum;//新建变量l、r、sum
        while (l < r) {
            sum = numbers[l] + numbers[r];
            if (sum == target) break;
            if (sum < target) ++l;//和小于sum时把较小数增大
            else --r;//反之把较大数减小
        }
        return new int[]{l + 1, r + 1};//返回新建数组元素指针从0开始需要加1
    }
}

执行用时: 1 ms 内存消耗: 38.6 MB

官方题解:

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int low = 0, high = numbers.length - 1;
        while (low < high) {
            int sum = numbers[low] + numbers[high];
            if (sum == target) {
                return new int[]{low + 1, high + 1};
            } else if (sum < target) {
                ++low;
            } else {
                --high;
            }
        }
        return new int[]{-1, -1};
    }
}

执行用时: 1 ms 内存消耗: 38.8 MB

  1. 平方数之和
    给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c 。
    示例 1:
    输入:c = 5
    输出:true
    解释:1 * 1 + 2 * 2 = 5

题解:
使用方向相反的两个指针,假设这两个数存在,最大范围为0~sqrt©,指针l指向最左侧0,向右遍历,指针r指向sqrt©取整,向左遍历。

class Solution {
    public boolean judgeSquareSum(int c) {
        int l = 0, sum;
        int r = (int) Math.sqrt(c);
        while (l <= r) {
            sum = l * l + r * r;
            if (sum == c) return true;
            if (sum < c) ++l;
            else --r;
        }
        return false;
    }
}

执行用时: 2 ms 内存消耗: 35.1 MB

官方题解:

class Solution {
    public boolean judgeSquareSum(int c) {
        long left = 0;
        long right = (long) Math.sqrt(c);
        while (left <= right) {
            long sum = left * left + right * right;
            if (sum == c) {
                return true;
            } else if (sum > c) {
                right--;
            } else {
                left++;
            }
        }
        return false;
    }
}

执行用时: 4 ms 内存消耗: 35.2 MB

  1. 验证回文字符串 Ⅱ
    给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
    示例 1:
    输入: “aba”
    输出: True

题解:
1.创建两个指针,左指针l指向0,向右遍历,右指针r指向最右,向左遍历;
2.比较两个字符是否相等,返回相应的三种情况;
3.创建新方法isPalindrome()判断删去一个字符后的字符串是否时回文字符串。

class Solution {
    public boolean validPalindrome(String s) {
        int l = 0, r = s.length() - 1;
        while (l < r && s.charAt(l) == s.charAt(r)) {
            ++l;
            --r;
        }
        //返回三种情况,第一种s本身即为回文字符串,第二种为判断删去一个字符的情况
        return l >= r || isPalindrome(s, l, r - 1) || isPalindrome(s, l + 1, r); 
    }
    public boolean isPalindrome(String s, int l, int r) {
        while (l < r && s.charAt(l) == s.charAt(r)) {
            ++l;
            --r;
        }
        return l >= r;
    }
}

执行用时: 7 ms 内存消耗: 38.5 MB

官方题解:

class Solution {
    public boolean validPalindrome(String s) {
        int low = 0, high = s.length() - 1;
        while (low < high) {
            char c1 = s.charAt(low), c2 = s.charAt(high);
            if (c1 == c2) {
                ++low;
                --high;
            } else {
                return validPalindrome(s, low, high - 1) || validPalindrome(s, low + 1, high);
            }
        }
        return true;
    }

    public boolean validPalindrome(String s, int low, int high) {
        for (int i = low, j = high; i < j; ++i, --j) {
            char c1 = s.charAt(i), c2 = s.charAt(j);
            if (c1 != c2) {
                return false;
            }
        }
        return true;
    }
}

执行用时: 7 ms 内存消耗: 39 MB

3.3 归并有序数组

  1. 合并两个有序数组
    给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
    初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
    示例 1:
    输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
    输出:[1,2,2,3,5,6]

题解:
把两个指针分别放在两个数组的末尾,即 nums1 的m − 1 位和 nums2 的 n − 1位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。直接利用m和n当作两个数组的指针,在创建一个pos用于确定新数组nums1的指针位置。

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int pos = m-- + n-- -1;//pos起始位置为m+n-1
        while (m >= 0 && n >= 0) {
            nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];//两数组中较大的元素加入到nums1中
        }
        while (n >= 0) {//nums2还有剩余数组
            nums1[pos--] = nums2[n--];
        }
    }
}

执行用时: 0 ms 内存消耗: 38.8 MB

官方题解:

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int p1 = 0, p2 = 0;
        int[] sorted = new int[m + n];
        int cur;
        while (p1 < m || p2 < n) {
            if (p1 == m) {
                cur = nums2[p2++];
            } else if (p2 == n) {
                cur = nums1[p1++];
            } else if (nums1[p1] < nums2[p2]) {
                cur = nums1[p1++];
            } else {
                cur = nums2[p2++];
            }
            sorted[p1 + p2 - 1] = cur;
        }
        for (int i = 0; i != m + n; ++i) {
            nums1[i] = sorted[i];
        }
    }
}

执行用时: 0 ms 内存消耗: 38.6 MB

  1. 通过删除字母匹配到字典里最长单词
    给你一个字符串 s 和一个字符串数组 dictionary 作为字典,找出并返回字典中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。
    如果答案不止一个,返回长度最长且字典序最小的字符串。如果答案不存在,则返回空字符串。
    示例 1:
    输入:s = “abpcplea”, dictionary = [“ale”,“apple”,“monkey”,“plea”]
    输出:“apple”

3.4 快慢指针

  1. 环形链表 II
    给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
    为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
    说明:不允许修改给定的链表。
    进阶:
    你是否可以使用 O(1) 空间解决此题?
    示例 1:
    输入:head = [3,2,0,-4], pos = 1
    输出:返回索引为 1 的链表节点
    解释:链表中有一个环,其尾部连接到第二个节点。在这里插入图片描述
    题解:
    给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步, slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。
/**
 * 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;//创建快慢指针
        //判断是否存在环路
        do {
            if (fast == null || fast.next == null) return null;//不存在结点或只有一个结点返回null
            fast = fast.next.next;//fast移动两个位置
            slow = slow.next; //slow移动一个位置
        } while (fast != slow);//不断循环当快慢指针不能相遇则不存在环路跳出循环
        fast = head;//第一次相遇将fast指针移动到链表开头
        //查找环路结点,快慢指针都只移动一个位置第二次相遇位置即为结点
        while (fast != slow) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}

执行用时: 0 ms 内存消耗: 38.7 MB

官方题解:

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode slow = head, fast = head;
        while (fast != null) {
            slow = slow.next;
            if (fast.next != null) {
                fast = fast.next.next;
            } else {
                return null;
            }
            if (fast == slow) {
                ListNode ptr = head;
                while (ptr != slow) {
                    ptr = ptr.next;
                    slow = slow.next;
                }
                return ptr;
            }
        }
        return null;
    }
}

执行用时: 0 ms 内存消耗: 38.7 MB

3.5 滑动窗口

  1. 最小覆盖子串
    给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
    注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
    示例 1:
    输入:s = “ADOBECODEBANC”, t = “ABC”
    输出:“BANC”

题解:
1.遍历字符串t并记录需要的字符及其个数,用数组need映射;
2.在字符串s中建立滑动窗口,左边界l,有边界r,不断增加r使滑动窗口增大直到包含了t中的所有元素,need中所有元素<=0且count也为0;
3.不断增加l减小滑动窗口直到碰到第一个必须包含的元素,记录此时的长度;
4.再增加i,重复上述步骤直到滑动窗口再一次满足要求。

class Solution {
    public String minWindow(String s, String t) {
    	//特殊判空情况
        if (s == null || s.length() == 0 || t == null || t.length() == 0){
            return "";
        }
        //用长度128的数组need映射字符
        int[] need = new int[128];
        //遍历字符串t,记录需要的字符的个数
        for (int i = 0; i < t.length(); i++) {
            need[t.charAt(i)]++;
        }
        //l是当前左边界,r是当前右边界,size记录窗口大小,count是需求的字符个数,start是最小覆盖串开始的index
        int l = 0, r = 0, size = Integer.MAX_VALUE, count = t.length(), start = 0;
        //遍历s的所有字符
        while (r < s.length()) {
            if (need[s.charAt(r)] > 0) {
                count--;
            }
            need[s.charAt(r)]--;//把右边的字符加入窗口,s中的字符减少1
            if (count == 0) {//窗口中已经包含所有字符
                while (l < r && need[s.charAt(l)] < 0) {
                    need[s.charAt(l)]++;//释放右边移动出窗口的字符
                    l++;//指针右移
                }
                if (r - l + 1 < size) {//不能右移时候挑战最小窗口大小,更新最小窗口开始的start
                    size = r - l + 1;
                    start = l;//记录下最小值时候的开始位置,最后返回覆盖串时候会用到
                }
                //l向右移动后窗口肯定不能满足了 重新开始循环
                need[s.charAt(l)]++;
                l++;
                count++;
            }
            r++;
        }
        return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
    }
}

官方题解:

class Solution {
    Map<Character, Integer> ori = new HashMap<Character, Integer>();
    Map<Character, Integer> cnt = new HashMap<Character, Integer>();

    public String minWindow(String s, String t) {
        int tLen = t.length();
        for (int i = 0; i < tLen; i++) {
            char c = t.charAt(i);
            ori.put(c, ori.getOrDefault(c, 0) + 1);
        }
        int l = 0, r = -1;
        int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;
        int sLen = s.length();
        while (r < sLen) {
            ++r;
            if (r < sLen && ori.containsKey(s.charAt(r))) {
                cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);
            }
            while (check() && l <= r) {
                if (r - l + 1 < len) {
                    len = r - l + 1;
                    ansL = l;
                    ansR = l + len;
                }
                if (ori.containsKey(s.charAt(l))) {
                    cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
                }
                ++l;
            }
        }
        return ansL == -1 ? "" : s.substring(ansL, ansR);
    }

    public boolean check() {
        Iterator iter = ori.entrySet().iterator(); 
        while (iter.hasNext()) { 
            Map.Entry entry = (Map.Entry) iter.next(); 
            Character key = (Character) entry.getKey(); 
            Integer val = (Integer) entry.getValue(); 
            if (cnt.getOrDefault(key, 0) < val) {
                return false;
            }
        } 
        return true;
    }
}

4 二分查找

4.1 算法概念

二分查找(Binary Search)也叫作折半查找。二分查找有两个要求,一个是数列有序,另一个是数列使用顺序存储结构(比如数组)。长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。

4.2 开平方

  1. x 的平方根
    实现 int sqrt(int x) 函数。
    计算并返回 x 的平方根,其中 x 是非负整数。
    由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
    示例 1:
    输入: 4
    输出: 2

题解:
将开方转化为给定一个非负整数x,求 f ® = r^2 − x = 0 的解。
单独考虑x == 0的情况,然后再[1,x]区间内用二分查找法。

class Solution {
    public int mySqrt(int x) {
        if (x == 0) return x;
        int l = 1, r = x, mid, sqrt;
        while (l <= r) {
            mid = l + (r - l) /2;
            sqrt = x / mid;
            if (sqrt == mid) {
                return mid;
            } else if (mid > sqrt) {
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return r;
    }
}

执行用时: 1 ms 内存消耗: 35.4 MB

官方题解:

class Solution {
    public int mySqrt(int x) {
        int l = 0, r = x, ans = -1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if ((long) mid * mid <= x) {
                ans = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return ans;
    }
}

执行用时: 1 ms 内存消耗: 35.6 MB

4.3 查找区间

  1. 在排序数组中查找元素的第一个和最后一个位置
    给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
    如果数组中不存在目标值 target,返回 [-1, -1]。
    进阶:
    你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
    示例 1:
    输入:nums = [5,7,7,8,8,10], target = 8
    输出:[3,4]

题解:
用二分法分别查找数组中的target。

class Solution {
    public int[] searchRange(int[] nums, int target) {
        if (nums == null || nums.length == 0) return new int[]{-1, -1};
        int lower = lower_bound(nums, target);
        int upper = upper_bound(nums, target) - 1;
        if (lower == nums.length || nums[lower] != target) {
            return new int[]{-1, -1};
        }
        return new int[]{lower, upper};
    }
	//二分查找序号低的target
    int lower_bound(int[] nums, int target) {
        int l = 0, r = nums.length, mid;
        while (l < r) {
            mid = (l + r) / 2;
            if (nums[mid] >= target) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return l;
    }
    //二分查找序号高的target
     int upper_bound(int[] nums, int target) {
         int l = 0, r = nums.length, mid;
         while (l < r) {
            mid = (l + r) / 2;
            if (nums[mid] > target) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return l;
     }
}

执行用时: 0 ms 内存消耗: 41.7 MB

官方题解:

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int leftIdx = binarySearch(nums, target, true);
        int rightIdx = binarySearch(nums, target, false) - 1;
        if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] == target && nums[rightIdx] == target) {
            return new int[]{leftIdx, rightIdx};
        } 
        return new int[]{-1, -1};
    }

    public int binarySearch(int[] nums, int target, boolean lower) {
        int left = 0, right = nums.length - 1, ans = nums.length;
        while (left <= right) {
            int mid = (left + right) / 2;
            if (nums[mid] > target || (lower && nums[mid] >= target)) {
                right = mid - 1;
                ans = mid;
            } else {
                left = mid + 1;
            }
        }
        return ans;
    }
}

执行用时: 0 ms 内存消耗: 41.4 MB

  1. 有序数组中的单一元素
    给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
    示例 1:
    输入: [1,1,2,3,3,4,4,8,8]
    输出: 2
    注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。

题解:

class Solution {
    public int singleNonDuplicate(int[] nums) {
        int l = 0, r = nums.length - 1;
        while (l < r) {
            int mid = l + (r - l) / 2;
            //mid为偶数且与后一个元素相等,只出现一次元素在mid右侧
            if (mid % 2 == 0 && nums[mid] == nums[mid + 1]) {
                l = mid + 2;
        	//mid为奇数且与前一个元素相等,只出现一次元素在mid右侧
            } else if (mid % 2 == 1 && nums[mid - 1] == nums[mid]) {
                l = mid + 1;
            //只出现一次元素在左侧
            } else {
                r = mid;
            }
        }
        return nums[l];
    }
}

执行用时: 0 ms 内存消耗: 38.7 MB

官方题解:

class Solution {
    public int singleNonDuplicate(int[] nums) {
        int lo = 0;
        int hi = nums.length - 1;
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (mid % 2 == 1) mid--;
            if (nums[mid] == nums[mid + 1]) {
                lo = mid + 2;
            } else {
                hi = mid;
            }
        }
        return nums[lo];
    }
}

执行用时: 0 ms 内存消耗: 38.5 MB

4.4 旋转数组

  1. 搜索旋转排序数组 II
    已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
    在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
    给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
    示例 1:
    输入:nums = [2,5,6,0,0,1,2], target = 0
    输出:true
    进阶:
    这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
    这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?

题解:
数组旋转后,仍旧保留部分有序和递增,利用这个特性进行二分查找。
对于当前mid,如果该值<=右侧,那么右侧有序,反之左侧有序。
如果target位于有序区间内,继续对这个区间二分查找,反之对另一半区间二分查找。
因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。

class Solution {
    public boolean search(int[] nums, int target) {
        int start = 0, end = nums.length - 1;
        while (start <= end) {
            int mid = (start + end) / 2;
            //恰好找到target直接返回true
            if (nums[mid] == target) {
                return true;
            }
            if (nums[start] == nums[mid]) {
            //无法判断增序
                ++start;
            } else if (nums[mid] <= nums[end]) {
            //右区间增序
                if (target > nums[mid] && target <= nums[end]) {
                    start = mid + 1;
                } else {
                    end = mid - 1;
                }
            } else {
            //左区间增序
                if (target >= nums[start] && target < nums[mid]) {
                    end = mid - 1;
                } else {
                    start = mid + 1;
                }
            }
        }
        return false;
    }
}

执行用时: 1 ms 内存消耗: 38.1 MB

官方题解:

class Solution {
    public boolean search(int[] nums, int target) {
        int n = nums.length;
        if (n == 0) {
            return false;
        }
        if (n == 1) {
            return nums[0] == target;
        }
        int l = 0, r = n - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target) {
                return true;
            }
            if (nums[l] == nums[mid] && nums[mid] == nums[r]) {
                ++l;
                --r;
            } else if (nums[l] <= nums[mid]) {
                if (nums[l] <= target && target < nums[mid]) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                if (nums[mid] < target && target <= nums[n - 1]) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }
        return false;
    }
}

执行用时: 1 ms 内存消耗: 37.6 MB

  1. 寻找旋转排序数组中的最小值 II
    已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
    若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
    若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
    注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
    给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
    示例 1:
    输入:nums = [1,3,5]
    输出:1

题解:
当 nums[mid] > nums[right]时,最小值一定在mid右侧,此时i一定满足 mid < i <= right,因此执行 left = mid + 1;
当 nums[mid] < nums[right] 时,最小值一定在mid左侧,此时i一定满足 left < i <= mid,因此执行 right = mid;
当 nums[mid] == nums[right] 时,有重复元素无法判断,因此将最右减-1.

class Solution {
    public int findMin(int[] nums) {
        int left = 0, right = nums.length - 1;
        while (left < right) {
            int mid = (left + right) / 2;
            //最小值在右侧
            if (nums[mid] > nums[right]) left = mid + 1;
            //最小值在左侧
            else if (nums[mid] < nums[right]) right = mid;
            //重复数字无法判断
            else right = right - 1;
        }
        return nums[left];
    }
}

执行用时: 0 ms 内存消耗: 38.1 MB

官方题解:

class Solution {
    public int findMin(int[] nums) {
        int low = 0;
        int high = nums.length - 1;
        while (low < high) {
            int pivot = low + (high - low) / 2;
            if (nums[pivot] < nums[high]) {
                high = pivot;
            } else if (nums[pivot] > nums[high]) {
                low = pivot + 1;
            } else {
                high -= 1;
            }
        }
        return nums[low];
    }
}

执行用时: 1 ms 内存消耗: 38.2 MB

5 排序算法

5.1 排序算法大全

在这里插入图片描述

5.2 快速选择

  1. 数组中的第K个最大元素
    在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
    示例 1:
    输入: [3,2,1,5,6,4] 和 k = 2
    输出: 5

题解:

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int l = 0, r = nums.length - 1, target = nums.length - k;
        while (l < r) {
            int mid = quickSelection(nums, l, r);
            if (mid == target) {
                return nums[mid];
            }
            if (mid < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return nums[l];
    }

    private int quickSelection(int[] nums, int l, int r) {
        int i = l + 1, j = r;
        while (true) {
            while (i < r && nums[i] <= nums[l]) {
                ++i;
            }
            while (l < j && nums[j] >= nums[l]) {
                --j;
            }
            if (i >= j) {
                break;
            }
            swap(nums, i, j);
        }
        swap(nums, l, j);
        return j;
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

执行用时: 18 ms 内存消耗: 38.1 MB

官方题解:
改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q 正好就是我们需要的下标,就直接返回 a[q]a[q];否则,如果 qq 比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。

class Solution {
    Random random = new Random();

    public int findKthLargest(int[] nums, int k) {
        return quickSelect(nums, 0, nums.length - 1, nums.length - k);
    }

    public int quickSelect(int[] a, int l, int r, int index) {
        int q = randomPartition(a, l, r);
        if (q == index) {
            return a[q];
        } else {
            return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
        }
    }

    public int randomPartition(int[] a, int l, int r) {
        int i = random.nextInt(r - l + 1) + l;
        swap(a, i, r);
        return partition(a, l, r);
    }

    public int partition(int[] a, int l, int r) {
        int x = a[r], i = l - 1;
        for (int j = l; j < r; ++j) {
            if (a[j] <= x) {
                swap(a, ++i, j);
            }
        }
        swap(a, i + 1, r);
        return i + 1;
    }

    public void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

执行用时: 2 ms 内存消耗: 38.5 MB

5.3 桶排序

  1. 前 K 个高频元素
    给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
    示例 1:
    输入: nums = [1,1,1,2,2,3], k = 2
    输出: [1,2]

题解:
首先依旧使用哈希表统计频率,统计完成后,创建一个数组,将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标即可。

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        List<Integer> res = new ArrayList();
        // 使用字典,统计每个元素出现的次数,元素为键,元素出现的次数为值
        HashMap<Integer,Integer> map = new HashMap();
        for(int num : nums){
            if (map.containsKey(num)) {
               map.put(num, map.get(num) + 1);
             } else {
                map.put(num, 1);
             }
        }
        
        //桶排序
        //将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
        List<Integer>[] list = new List[nums.length+1];
        for(int key : map.keySet()){
            // 获取出现的次数作为下标
            int i = map.get(key);
            if(list[i] == null){
               list[i] = new ArrayList();
            } 
            list[i].add(key);
        }
        
        // 倒序遍历数组获取出现顺序从大到小的排列
        for(int i = list.length - 1;i >= 0 && res.size() < k;i--){
            if(list[i] == null) continue;
            res.addAll(list[i]);
        }
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
}

执行用时: 16 ms 内存消耗: 40.5 MB

  1. 根据字符出现频率排序
    给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
    示例 1:
    输入:
    “tree”
    输出:
    “eert”
    解释:
    'e’出现两次,'r’和’t’都只出现一次。
    因此’e’必须出现在’r’和’t’之前。此外,"eetr"也是一个有效的答案。

题解:

class Solution {
    public String frequencySort(String s) {
        List<String> res = new ArrayList();
        HashMap<String, Integer> map = new HashMap();
        for (int i = 0; i < s.length(); ++i) {
            if (map.containsKey(s.substring(i, i + 1))) {
                map.put(s.substring(i, i + 1), map.get(s.substring(i, i + 1)) + 1);
            } else {
                map.put(s.substring(i, i + 1), 1);
            }
        }

        List<String>[] list = new List[s.length() + 1];
        for (String key : map.keySet()) {
            int i = map.get(key);
            if (list[i] == null) list[i] = new ArrayList();
            list[i].add(key);
        }

        for (int i = list.length - 1; i >= 0; --i) {
            if (list[i] == null) continue;
            res.addAll(list[i]);
        }
        return String.join("", res);
    }
}

6 图的搜索

6.1 深度优先搜索(DFS)

深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。遍历需要用先入后出的栈来实现。

  1. 岛屿的最大面积
    给定一个包含了一些 0 和 1 的非空二维数组 grid 。
    一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
    找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
    示例 1:
    [[0,0,1,0,0,0,0,1,0,0,0,0,0],
    [0,0,0,0,0,0,0,1,1,1,0,0,0],
    [0,1,1,0,1,0,0,0,0,0,0,0,0],
    [0,1,0,0,1,1,0,0,1,0,1,0,0],
    [0,1,0,0,1,1,0,0,1,1,1,0,0],
    [0,0,0,0,0,0,0,0,0,0,1,0,0],
    [0,0,0,0,0,0,0,1,1,1,0,0,0],
    [0,0,0,0,0,0,0,1,1,0,0,0,0]]
    对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。

题解:
深度优先搜索+递归
(1)maxAreaOfIsland()方法用于遍历所有的搜索位置,判断是否可以开始搜索;
(2)dfs()方法用于深度优先搜索的递归调用。

class Solution {
	//四个方向遍历创建一个数组[-1,0,1,0,-1],每相邻两位即为上下左右四个方向之一。
    int[] direction = new int[]{-1, 0, 1, 0, -1};
    //遍历数组所有搜索的位置
    public int maxAreaOfIsland(int[][] grid) {
        if (grid == null || grid[0] == null) return 0;
        int max_area = 0;
        for (int i = 0; i < grid.length; ++i) {
            for (int j = 0; j < grid[0].length; ++j) {
                if (grid[i][j] == 1) {
                    max_area = Math.max(max_area, dfs(grid, i, j));
                }
            }
        }
        return max_area;
    }
    //深度优先搜索
    int dfs(int[][] grid, int r, int c) {
        if (grid[r][c] == 0) return 0;
        grid[r][c] = 0;
        int x, y, area = 1;
        for (int i = 0; i < 4; ++i) {
            x = r + direction[i];
            y = c + direction[i + 1];
            if (x >= 0 && x < grid.length && y >= 0 && y < grid[0].length) {
                area += dfs(grid, x, y);
            }
        }
        return area;
    }
}

执行用时: 3 ms 内存消耗: 39 MB

官方题解:
深度优先搜索+栈

class Solution {
    public int maxAreaOfIsland(int[][] grid) {
        int ans = 0;
        for (int i = 0; i != grid.length; ++i) {
            for (int j = 0; j != grid[0].length; ++j) {
                int cur = 0;
                Deque<Integer> stacki = new LinkedList<Integer>();
                Deque<Integer> stackj = new LinkedList<Integer>();
                stacki.push(i);
                stackj.push(j);
                while (!stacki.isEmpty()) {
                    int cur_i = stacki.pop(), cur_j = stackj.pop();
                    if (cur_i < 0 || cur_j < 0 || cur_i == grid.length || cur_j == grid[0].length || grid[cur_i][cur_j] != 1) {
                        continue;
                    }
                    ++cur;
                    grid[cur_i][cur_j] = 0;
                    int[] di = {0, 0, 1, -1};
                    int[] dj = {1, -1, 0, 0};
                    for (int index = 0; index != 4; ++index) {
                        int next_i = cur_i + di[index], next_j = cur_j + dj[index];
                        stacki.push(next_i);
                        stackj.push(next_j);
                    }
                }
                ans = Math.max(ans, cur);
            }
        }
        return ans;
    }
}
  1. 省份数量
    有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
    省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
    给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
    返回矩阵中 省份 的数量。
    示例 1:
    在这里插入图片描述
    输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
    输出:2

个人题解:
图的表示方法:每一行(列)表示要给结点,它的每列(行)表示是否存在一个相邻结点。n x n 的矩阵 isConnected表示有n个结点,每个结点最多有n条边,表示和该城市和所有城市相连,最少只有一条边,表示该城市自成一省。

class Solution {
	//遍历数组中的每个结点
    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length, count = 0;
        //visited记录每个城市是否被访问过,默认值为false
        boolean[] visited = new boolean[n];
        for (int i = 0; i < n; ++i) {
        	//i行已经被访问过
            if (!visited[i]) {
                dfs(isConnected, i, visited);
                ++count;
            }
        }
        return count;
    }
	//对i行进行深度优先搜索
    void dfs(int[][] isConnected, int i, boolean[] visited) {
        visited[i] = true;
        //遍历第k列
        for (int k = 0; k < isConnected.length; ++k) {
        	//该节点为1并且该节点没有被访问过
            if (isConnected[i][k] == 1 && !visited[k]) {
                dfs(isConnected, k, visited);
            }
        }
    }
}

执行用时: 1 ms 内存消耗: 39.4 MB

官方题解:

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int provinces = isConnected.length;
        boolean[] visited = new boolean[provinces];
        int circles = 0;
        for (int i = 0; i < provinces; i++) {
            if (!visited[i]) {
                dfs(isConnected, visited, provinces, i);
                circles++;
            }
        }
        return circles;
    }

    public void dfs(int[][] isConnected, boolean[] visited, int provinces, int i) {
        for (int j = 0; j < provinces; j++) {
            if (isConnected[i][j] == 1 && !visited[j]) {
                visited[j] = true;
                dfs(isConnected, visited, provinces, j);
            }
        }
    }
}

执行用时: 1 ms 内存消耗: 39.5 MB

  1. 太平洋大西洋水流问题
    平洋”,又能流动到“大西洋”的陆地单元的坐标。
    提示:
    输出坐标的顺序不重要
    m 和 n 都小于150
    示例:
    给定下面的 5x5 矩阵:
    太平洋 ~ ~ ~ ~ ~
    ~ 1 2 2 3 (5) *
    ~ 3 2 3 (4) (4) *
    ~ 2 4 (5) 3 1 *
    ~ (6) (7) 1 4 5 *
    ~ (5) 1 1 2 4 *
    * * * * * 大西洋
    返回:
    [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).

题解:
1.建立两个矩阵Atlantic和Pacific, 当Atlantic[i][j]和Pacific[i][j]同时为true时表示该位置可以同时到达Atlantic和Pacific;
2.只需要从四个边界开始遍历即可(类似泛洪的思想, 只要可以从边界出发到达, 就说明该位置的水可以流向对应的海洋)

class Solution {
    public List<List<Integer>> pacificAtlantic(int[][] heights) {
        List<List<Integer>> ret = new ArrayList<>();
        int m = heights.length, n = heights[0].length;
        if (m < 1) return ret;
        //新建两个二维数组分别代表太平洋二号大西洋
        boolean[][] Pacific = new boolean[m][n];
        boolean[][] Atlantic = new boolean[m][n];
        for (int i = 0; i < m; ++i) {
            dfs(heights, i, 0, Pacific, heights[i][0]);
            dfs(heights, i, n-1, Atlantic, heights[i][n-1]);
        }
        for (int i = 0; i < n; ++i) {
            dfs(heights, 0, i, Pacific, heights[0][i]);
            dfs(heights, m-1, i, Atlantic, heights[m-1][i]);
        }
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
            //两个数组都为true则水可以到达
                if (Pacific[i][j] && Atlantic[i][j]) {
                    List<Integer> list = new ArrayList<Integer>();
		            Collections.addAll(list, i, j);
                    ret.add(list);
                }
            }
        }
        return ret;
    }
    
    private void dfs(int[][] h, int x, int y, boolean[][] visited, int pre) {
        if (x < 0 || y < 0 || x >= m.length || y >= m[0].length || visited[x][y] || m[x][y] < pre) return;
        visited[x][y] = true;
        //四个边界遍历
        dfs(h, x+1, y, visited, m[x][y]);
        dfs(h, x-1, y, visited, m[x][y]);
        dfs(h, x, y+1, visited, m[x][y]);
        dfs(h, x, y-1, visited, m[x][y]);
    }
}

执行用时: 5 ms 内存消耗: 39.3 MB

6.2 回溯法(backtracking)

回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

  1. 全排列
    给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
    示例 1:
    输入:nums = [1,2,3]
    输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

题解:
递归结构:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
状态变量:
List<List<Integer>> res :用于储存所有全排列
List<Integer> path:表示当前状态,当向下一层移动时,path变量在尾部add,返回上一层时,撤销上一次的选择,后进先出的数据结构,是一个栈;
int depth:表示当前程序递归的层数;
boolean[] used:表示该位置的数是否被选择,默认false 表示这些数还没有被选择,当选定一个数后该位置变量值为true;

public class Solution {

    public List<List<Integer>> permute(int[] nums) {
        // 首先是特判
        int len = nums.length;
        // 使用一个动态数组保存所有可能的全排列
        List<List<Integer>> res = new ArrayList<>();

        if (len == 0) {
            return res;
        }

        boolean[] used = new boolean[len];
        List<Integer> path = new ArrayList<>();

        dfs(nums, len, 0, path, used, res);
        return res;
    }

    private void dfs(int[] nums, int len, int depth,
                     List<Integer> path, boolean[] used,
                     List<List<Integer>> res) {
        if (depth == len) {
            //不用拷贝,因为每一层传递下来的 path 变量都是新建的
            res.add(path);
            return;
        }

        for (int i = 0; i < len; i++) {
            if (!used[i]) {
                //每一次尝试都创建新的变量表示当前的"状态"
                List<Integer> newPath = new ArrayList<>(path);
                newPath.add(nums[i]);

                boolean[] newUsed = new boolean[len];
                System.arraycopy(used, 0, newUsed, 0, len);
                newUsed[i] = true;

                dfs(nums, len, depth + 1, newPath, newUsed, res);
                //无需回溯
            }
        }
    }
}

执行用时: 1 ms 内存消耗: 38.3 MB

官方题解:

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<List<Integer>>();

        List<Integer> output = new ArrayList<Integer>();
        for (int num : nums) {
            output.add(num);
        }

        int n = nums.length;
        backtrack(n, output, res, 0);
        return res;
    }

    public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
        // 所有数都填完了
        if (first == n) {
            res.add(new ArrayList<Integer>(output));
        }
        for (int i = first; i < n; i++) {
            // 动态维护数组
            Collections.swap(output, first, i);
            // 继续递归填下一个数
            backtrack(n, output, res, first + 1);
            // 撤销操作
            Collections.swap(output, first, i);
        }
    }
}

执行用时: 1 ms 内存消耗: 38.7 MB

  1. 组合
    给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
    示例:
    输入: n = 4, k = 2
    输出:
    [
    [2,4],
    [3,4],
    [2,3],
    [1,2],
    [1,3],
    [1,4],
    ]

题解:
递归结构:在以n为结尾的候选数组里,选出k个元素。
状态变量:
List<List<Integer>> res:储存返回的数组;
int begin:搜索起点,表示在区间 [begin, n] 里选出若干个数的组合;
Deque<Integer> path:表示路径的变量,是一个列表,用栈方式储存;
剪枝:当数组中的剩余元素小于k时已经不满足情况,不需要继续遍历下去,对深度优先搜索遍历范围进行剪枝。
总结规律可得:搜索起点的最大值 = n - (k - path.size()) + 1

public class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        if (k <= 0 || n < k) {
            return res;
        }
        Deque<Integer> path = new ArrayDeque<>();
        //题目设定从1开始搜索
        dfs(n, k, 1, path, res);
        return res;
    }

    private void dfs(int n, int k, int begin, Deque<Integer> path, List<List<Integer>> res) {
    	//当path等于k时找到满足要求数组,递归终止
        if (path.size() == k) {
            res.add(new ArrayList<>(path));
            return;
        }
        //遍历所有搜索起点
        for (int i = begin; i <= n - (k - path.size()) + 1; ++i) {
            path.addLast(i);
            //下一轮路径变量加1,不允许重复
            dfs(n, k, i + 1, path, res);
            //返回到上一层
            path.removeLast();
        }
    }
}

执行用时: 2 ms 内存消耗: 39.5 MB

  1. 单词搜索
    给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
    单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
    示例 1:
    在这里插入图片描述
    输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
    输出:true

题解:
递归结构:遍历上下左右元素与word中相应字符进行匹配;
状态变量:
int x:board中的行;
int y:board中的列;
int begin:word中的开始位置;
boolean[][] visited:记录当前节点状态

public class Solution {
	//新建偏移量数组DIRECTIONS
    private static final int[][] DIRECTIONS = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
    private int rows;//行
    private int cols;//列
    private int len;//word长度
    private boolean[][] visited;//节点是否被访问变量
    private char[] charArray;//word字符转换
    private char[][] board;

    public boolean exist(char[][] board, String word) {
        rows = board.length;
        if (rows == 0) {
            return false;
        }
        cols = board[0].length;
        visited = new boolean[rows][cols];

        this.len = word.length();
        this.charArray = word.toCharArray();
        this.board = board;
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (dfs(i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean dfs(int x, int y, int begin) {
    	//结束条件
        if (begin == len - 1) {
            return board[x][y] == charArray[begin];
        }
        if (board[x][y] == charArray[begin]) {
            visited[x][y] = true;//修改当前结点状态
            for (int[] direction : DIRECTIONS) {//递归子节点
                int newX = x + direction[0];
                int newY = y + direction[1];
                if (inArea(newX, newY) && !visited[newX][newY]) {
                    if (dfs(newX, newY, begin + 1)) {
                        return true;
                    }
                }
            }
            visited[x][y] = false;//回退当前结点状态
        }
        return false;
    }

    private boolean inArea(int x, int y) {
        return x >= 0 && x < rows && y >= 0 && y < cols;
    }
}

执行用时: 106 ms 内存消耗: 36.6 MB

  1. N 皇后
    n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
    给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
    每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
    示例 1:
    在这里插入图片描述
    输入:n = 4
    输出:[[".Q…","…Q",“Q…”,"…Q."],["…Q.",“Q…”,"…Q",".Q…"]]
    解释:如上图所示,4 皇后问题存在两个不同的解法。

题解:
递归结构:将棋盘的行列转换为决策树,从树的根节点[]开始遍历,每一层表示棋盘中的每一行,该节点可以选择任意一列放置一个Queen;
状态变量:
int row:遍历位置的行;
int col:遍历位的列;
剪枝:
数组是从上往下遍历,当前行和下方没有放置Queen不需要检查,只需要检查左上方、上方、右上方三个方向。

class Solution {
    List<List<String>> res = new ArrayList<List<String>>();
    public List<List<String>> solveNQueens(int n) {
    	//初始化棋盘
        char[][] board = new char[n][n];
        for (char[] c : board) Arrays.fill(c, '.');

        backtracking(n, board, 0);
        return res;
    }
	//board[row][col]位置是否可以放置Queen
    public boolean isValid(char[][] board, int row, int col, int n) {
        for (int i = 0; i < row; ++i) {//检查该列中是否有Queen
            if (board[i][col] == 'Q') return false;
        }
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; --i, ++j) {//检查右上方是否有Queen
            if (board[i][j] == 'Q') return false;
        }
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {//检查左上方是否有Queen
            if (board[i][j] == 'Q') return false;
        }
        return true;
    }
	//将char[][]的行转换成List集合
   public List<String> Array2List(char[][] board) {
        List<String> list = new ArrayList<>();
        for (char[] c : board) {
            list.add(String.copyValueOf(c));
        }
        return list;
    }

    public void backtracking(int n, char[][] board, int row) {
    	//结束条件
        if (row == n) {
            res.add(Array2List(board));
            return;
        }
        //选择列表
        for (int col = 0; col < n; ++col) {
            if (!isValid(board, row, col, n)) continue;//该位置不能放置Queen继续遍历该行中的列
            board[row][col] = 'Q';//修改当前节点
            backtracking(n, board, row + 1);
            board[row][col] = '.';//回溯当前节点
        }
    }
}

执行用时: 3 ms 内存消耗: 38.4 MB

6.3 广度优先搜索(BFS)

广度优先搜索属于图算法的一种,英文缩写为BFS即Breadth First Search.,一层层遍历,遍历完一层再遍历下一层。遍历需要用先入先出的队列来实现。

  1. 最短的桥
    在给定的二维二进制数组 A 中,存在两座岛。(岛是由四面相连的 1 形成的一个最大组。)
    现在,我们可以将 0 变为 1,以使两座岛连接起来,变成一座岛。
    返回必须翻转的 0 的最小数目。(可以保证答案至少是 1 。)
    示例 1:
    输入:A = [[0,1],[1,0]]
    输出:1

题解:
本题转化为两个岛屿之间的最短距离,先深度优先搜索得到一个岛屿,然后广度优先搜索查找与另一个岛屿的最短距离。

class Solution {
    public int shortestBridge(int[][] A) {
    	//新建偏移量数组
        int [][] direction = new int [][]{{1,0},{-1,0},{0,1},{0,-1}};
        Deque<int[]> queue = new ArrayDeque<>();
        int ans = -1;
        boolean[][] visited = new boolean[A.length][A[0].length];
        boolean flag = true;
        for(int i = 0; i < A.length && flag; i++){
            for(int j = 0; j < A[0].length; j++) {
                if (A[i][j] == 1) {
                    dfs(A, i, j, queue, visited);
                    flag = false;
                    break;
                }
            }
        }
        while (!queue.isEmpty()){
            int size = queue.size();
            ans++;
            for(int i = 0; i < size; i++){
                int[] node = queue.poll();
                for(int j = 0; j < 4; j++){
                    int nx = node[0] + direction[j][0];
                    int ny = node[1] + direction[j][1];
                    if(nx<0 || nx >= A.length || ny<0 || ny>=A[0].length || visited[nx][ny])   continue;
                    if(A[nx][ny] == 1)    return ans;
                    visited[nx][ny] = true;
                    queue.add(new int[]{nx,ny});
                }
            }
        }
        return ans;
    }
    private void dfs(int[][] A, int i, int j, Deque queue, boolean[][] visited){
        if(i<0 || i>=A.length || j<0 || j>=A[0].length || visited[i][j] || A[i][j]!=1)   return;
        visited[i][j] = true;
        queue.add(new int []{i,j});
        dfs(A, i-1, j, queue, visited);
        dfs(A, i+1, j, queue, visited);
        dfs(A, i, j-1, queue, visited);
        dfs(A, i, j+1, queue, visited);
    }
}

执行用时: 9 ms 内存消耗: 39.1 MB

  1. 单词接龙 II
    按字典 wordList 完成从单词 beginWord 到单词 endWord 转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> … -> sk 这样的单词序列,并满足:
    每对相邻的单词之间仅有单个字母不同。
    转换过程中的每个单词 si(1 <= i <= k)必须是字典 wordList 中的单词。注意,beginWord 不必是字典 wordList 中的单词。
    sk == endWord
    给你两个单词 beginWord 和 endWord ,以及一个字典 wordList 。请你找出并返回所有从 beginWord 到 endWord 的 最短转换序列 ,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 [beginWord, s1, s2, …, sk] 的形式返回。
    示例 1:
    输入:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
    输出:[[“hit”,“hot”,“dot”,“dog”,“cog”],[“hit”,“hot”,“lot”,“log”,“cog”]]
    解释:存在 2 种最短的转换序列:
    “hit” -> “hot” -> “dot” -> “dog” -> “cog”
    “hit” -> “hot” -> “lot” -> “log” -> “cog”

题解:
把起始字符串、终止字符串、以及单词表里所有的字符串作为节点,则:
1)若两个字符串只有一个字符不同,则它们相连,BFS遍历构建图;
2)使用DFS求起始节点到终止节点的最短距离即为所求最短转换序列。

class Solution {
    public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
    	//新建集合储存返回值
        List<List<String>> res = new ArrayList<>();
        //将 wordList 存入哈希表命名为dict,快速判断单词是否存在wordList中
        Set<String> dict = new HashSet<>(wordList);
        //特殊情况判断
        if (!dict.contains(endWord)) {
            return res;
        }
        //dict中不包含beginWord
        dict.remove(beginWord);

		/**
		*BFS遍历构建图
		**/
		//创建集合steps记录已经访问过的word集合和在第几层访问到,其中key为单词,value为BFS的层数
        Map<String, Integer> steps = new HashMap<>();
        steps.put(beginWord, 0);
        //创建集合from记录单词从哪些单词扩展而得,其中key为单词,value为单词列表,两者关系为一对多
        Map<String, Set<String>> from = new HashMap<>();
        boolean found = bfs(beginWord, endWord, dict, steps, from);
		//DFS遍历找到所有解,从endWord恢复到beginWord
        if (found) {
        	//新建双端队列path
            Deque<String> path = new ArrayDeque<>();
            path.add(endWord);
            dfs(from, path, beginWord, endWord, res);
        }
        return res;
    }

    private boolean bfs(String beginWord, String endWord, Set<String> dict, Map<String, Integer> steps, Map<String, Set<String>> from) {
        int wordLen = beginWord.length();
        int step = 0;
        boolean found = false;

        Queue<String> queue = new LinkedList<>();
        queue.offer(beginWord);
        while (!queue.isEmpty()) {
            step++;
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String currWord = queue.poll();
                char[] charArray = currWord.toCharArray();
                for (int j = 0; j < wordLen; j++) {
                    char origin = charArray[j];
                    for (char c = 'a'; c <= 'z'; c++) {
                    	//每一位替换成26个小写字母
                        charArray[j] = c;
                        String nextWord = String.valueOf(charArray);
                        if (steps.containsKey(nextWord) && steps.get(nextWord) == step) {
                            from.get(nextWord).add(currWord);
                        }

                        if (!dict.contains(nextWord)) {
                            continue;
                        }
                        dict.remove(nextWord);
                        queue.offer(nextWord);
                        from.putIfAbsent(nextWord, new HashSet<>());
                        from.get(nextWord).add(currWord);
                        steps.put(nextWord, step);
                        if (nextWord.equals(endWord)) {
                            found = true;
                        }
                    }
                    charArray[j] = origin;
                }
            }
            if (found) {
                break;
            }
        }
        return found;
    }

    private void dfs(Map<String, Set<String>> from, Deque<String> path, String beginWord, String cur, List<List<String>> res) {
        if (cur.equals(beginWord)) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (String precursor : from.get(cur)) {
            path.addFirst(precursor);
            dfs(from, path, beginWord, precursor, res);
            path.removeFirst();
        }
    }
}

执行用时: 14 ms 内存消耗: 39 MB

7 动态规划

7.1 算法概念

定义
动态规划(Dynamic Programming, DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
解题套路
如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等。
解题思路
动态规划的核心思想就是拆分子问题,记住过往,减少重复计算,并且动态规划一般都是自底向上的。
1)穷举分析:
2)确定边界:找出特殊边界,确定动态规划的范围;
3)找出规律确定最优子结构:一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质;
4)写出状态转移方程

7.2 一维动态规划

  1. 爬楼梯
    假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
    每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
    注意:给定 n 是一个正整数。
    示例 1:
    输入: 2
    输出: 2
    解释: 有两种方法可以爬到楼顶。
    1. 1 阶 + 1 阶
    2. 2 阶

题解:
斐波那契数列问题,每次走一步或两步,则到达第i阶可以从i-1阶或i-2阶开始,可得到状态转移方程dp[i] = dp[i - 1] + dp[i - 2]

class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= n; ++i) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

执行用时: 0 ms 内存消耗: 35 MB

  1. 打家劫舍
    你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
    给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
    示例 1:
    输入:[1,2,3,1]
    输出:4
    解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
    偷窃到的最高金额 = 1 + 3 = 4 。

题解:
数组dp[i]表示抢劫到第i个房子时,可抢到的最大数量,有如下两种情况:
1)不抢劫第i个房子,则累计金额为dp[i-1];
2)抢劫第i个 房子,第i个房子序号为i-1,则累计金额为nums[i-1] + dp[i-2]。
状态转移方程为dp[i] = Math.max(dp[i-1], nums[i-1] + dp[i-2])

class Solution {
    public int rob(int[] nums) {
        if (nums == null) return 0;
        int n = nums.length;
        int[] dp = new int[n + 1];
        dp[1] = nums[0];
        for (int i = 2; i <= n; ++i) {
            dp[i] = Math.max(dp[i-1], nums[i-1] + dp[i-2]);
        } 
        return dp[n];
    }
}

执行用时: 0 ms 内存消耗: 35.9 MB

  1. 等差数列划分
    如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。
    例如,以下数列为等差数列:
    1, 3, 5, 7, 9
    7, 7, 7, 7
    3, -1, -5, -9
    以下数列不是等差数列。
    1, 1, 2, 5, 7
    数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。
    如果满足以下条件,则称子数组(P, Q)为等差数组:
    元素 A[P], A[p + 1], …, A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。
    函数要返回数组 A 中所有为等差数组的子数组个数。
    示例:
    A = [1, 2, 3, 4]
    返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。

题解:

class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        int sum = 0;
        for (int i = 2; i < n; ++i) {
        	//满足等差数列条件
            if (nums[i] - nums[i-1] == nums[i-1] - nums[i-2]) {
                dp[i] = dp[i-1] + 1;
                sum += dp[i];
            }
        } 
        return sum;
    }
}

执行用时: 0 ms 内存消耗: 36.3 MB

7.3 二维动态规划

  1. 最小路径和
    给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
    说明:每次只能向下或者向右移动一步。
    示例 1:
    在这里插入图片描述

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

题解:
穷举分析:假设此时要移动到grid[1][1] = 5,有两种情况,①从上面dp[0][1]移动到该位置 ,则此时最和为grid[0][0]+grid[0][1]+grid[1][1]=1+3+5=9;②从左侧dp[1][0]移动该位置,则此时最和为grid[0][0]+grid[1][0]+grid[1][1]=1+1+5=7;
确定边界:行和列都是从0开始;
最优子结构:假设第i行第j列的最小和为dp[i][j],可得上侧dp[i-1][j]和左侧dp[i][j-1]的较小值加上该位置的值;
状态转移方程:dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
空间压缩:矩阵中每一个值只与左侧或上侧值相关,可将dp数组压缩为一维数组,dp[j-1] = dp[i][j-1],dp[j]=dp[i-1][j],转移方程可写为dp[j] = Math.min(dp[j], dp[j-1]) + grid[i][j]

class Solution {
    public int minPathSum(int[][] grid) {
        int[] dp = new int[grid[0].length];
        for (int i = 0; i < grid.length; ++i) {
            for (int j = 0; j < grid[0].length; ++j) {
                if (i == 0 && j == 0) {
                    dp[j] = grid[i][j];
                } else if (i == 0) {
                    dp[j] = dp[j-1] + grid[i][j];
                } else if (j == 0) {
                    dp[j] = dp[j] + grid[i][j];
                } else {
                    dp[j] = Math.min(dp[j], dp[j-1]) + grid[i][j];
                }
            }
        }
        return dp[grid[0].length-1];
    }
}

执行用时: 3 ms 内存消耗: 41.1 MB

  1. 01 矩阵
    给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。
    两个相邻元素间的距离为 1 。
    示例 1:
    输入:
    [[0,0,0],
    [0,1,0],
    [0,0,0]]
    输出:
    [[0,0,0],
    [0,1,0],
    [0,0,0]]

题解:
穷举分析:matrix[1][1]上下左右四个方向到0的距离都是1;
​确定边界:从0到n;
最优子结构:最近的0的位置,是在左上,右上,左下,右上4个方向中的最小值;
状态转移方程:
在这里插入图片描述

class Solution {
  public int[][] updateMatrix(int[][] matrix) {
    int m = matrix.length, n = matrix[0].length;
    int[][] dp = new int[m][n];
    //和官方题解的差异设置dp[]的默认值增加了时间和空间
    for (int i = 0; i < m; i++) {
      for (int j = 0; j < n; j++) {
        dp[i][j] = matrix[i][j] == 0 ? 0 : 10000;
      }
    }

    // 从左上角开始(右上方已经包含其中)
    for (int i = 0; i < m; i++) {
      for (int j = 0; j < n; j++) {
      	//上侧
        if (i - 1 >= 0) {
          dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1);
        }
        //左侧
        if (j - 1 >= 0) {
          dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
        }
      }
    }
    // 从右下角开始(左下方已经包含其中)
    for (int i = m - 1; i >= 0; i--) {
      for (int j = n - 1; j >= 0; j--) {
      	//下侧
        if (i + 1 < m) {
          dp[i][j] = Math.min(dp[i][j], dp[i + 1][j] + 1);
        }
        //右侧
        if (j + 1 < n) {
          dp[i][j] = Math.min(dp[i][j], dp[i][j + 1] + 1);
        }
      }
    }
    return dp;
  }
}

执行用时: 9 ms 内存消耗: 41.9 MB

官方题解:

class Solution {
    static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    public int[][] updateMatrix(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        // 初始化动态规划的数组,所有的距离值都设置为一个很大的数
        int[][] dist = new int[m][n];
        for (int i = 0; i < m; ++i) {
            Arrays.fill(dist[i], Integer.MAX_VALUE / 2);
        }
        // 如果 (i, j) 的元素为 0,那么距离为 0
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (matrix[i][j] == 0) {
                    dist[i][j] = 0;
                }
            }
        }
        // 只有 水平向左移动 和 竖直向上移动,注意动态规划的计算顺序
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i - 1 >= 0) {
                    dist[i][j] = Math.min(dist[i][j], dist[i - 1][j] + 1);
                }
                if (j - 1 >= 0) {
                    dist[i][j] = Math.min(dist[i][j], dist[i][j - 1] + 1);
                }
            }
        }
        // 只有 水平向右移动 和 竖直向下移动,注意动态规划的计算顺序
        for (int i = m - 1; i >= 0; --i) {
            for (int j = n - 1; j >= 0; --j) {
                if (i + 1 < m) {
                    dist[i][j] = Math.min(dist[i][j], dist[i + 1][j] + 1);
                }
                if (j + 1 < n) {
                    dist[i][j] = Math.min(dist[i][j], dist[i][j + 1] + 1);
                }
            }
        }
        return dist;
    }
}

执行用时: 7 ms 内存消耗: 41.7 MB

  1. 最大正方形
    在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。
    示例 1:
    在这里插入图片描述
    输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
    输出:4

题解:
穷举分析:新建dp数组,dp[i][j]表示以(i,j)为右下角全由1构成的最大正方形,穷举见下图;
确定边界:行遍历1到m,列遍历1到n;
最优子结构:dp[i][j]的值为其左、左上、上三个位置的最小值,再加1;
状态转移方程:dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i][j-1], dp[i-1][j])) + 1
在这里插入图片描述

class Solution {
    public int maximalSquare(char[][] matrix) {
        if (matrix == null || matrix[0] == null) {
            return 0;
        }
        int m = matrix.length, n = matrix[0].length, max_side = 0;
        int[][] dp = new int[m+1][n+1];
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (matrix[i-1][j-1] == '1') {
                    dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i][j-1], dp[i-1][j])) + 1;
                }
                max_side = Math.max(max_side, dp[i][j]);
            }
        }
        return max_side * max_side;
    } 
}

执行用时: 6 ms 内存消耗: 41.6 MB

官方题解:

class Solution {
    public int maximalSquare(char[][] matrix) {
        int maxSide = 0;
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return maxSide;
        }
        int rows = matrix.length, columns = matrix[0].length;
        int[][] dp = new int[rows][columns];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < columns; 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]);
                }
            }
        }
        int maxSquare = maxSide * maxSide;
        return maxSquare;
    }
}

执行用时: 6 ms 内存消耗: 41.5 MB

7.4 分割问题

  1. 完全平方数
    给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
    给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
    完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
    示例 1:
    输入:n = 12
    输出:3
    解释:12 = 4 + 4 + 4

题解:

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        for (int i = 1; i <= n; ++i) {
            dp[i] = i;
            for (int j = 1; j * j <= i; ++j) {
                dp[i] = Math.min(dp[i], dp[i-j*j] + 1);
            }
        }
        return dp[n];
    }
}

执行用时: 41 ms 内存消耗: 37.6 MB

官方题解:

class Solution {
    public int numSquares(int n) {
        int[] f = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            int minn = Integer.MAX_VALUE;
            for (int j = 1; j * j <= i; j++) {
                minn = Math.min(minn, f[i - j * j]);
            }
            f[i] = minn + 1;
        }
        return f[n];
    }
}

执行用时: 30 ms 内存消耗: 37.5 MB

  1. 解码方法
    一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
    ‘A’ -> 1
    ‘B’ -> 2

    ‘Z’ -> 26
    要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
    “AAJF” ,将消息分组为 (1 1 10 6)
    “KJF” ,将消息分组为 (11 10 6)
    注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
    给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
    题目数据保证答案肯定是一个 32 位 的整数。
    示例 1:
    输入:s = “12”
    输出:2
    解释:它可以解码为 “AB”(1 2)或者 “L”(12)。

题解:
特殊边界:在字符串前加入空格避免出现两个数中前一个为0的情况,同时将边界定为从1开始,避免负数;
最优子结构:对于字符串s中的位置i,解码条件只有①i独立解码成字符,②i与i-1解码成字符;
状态转移方程:定义 f[i] 为考虑前 i个字符的解码方案数
f[i]=f[i−1],1⩽a≤9
f[i]=f[i−2],10⩽b⩽26
f[i]=f[i−1]+f[i−2],1⩽a≤9,10⩽b⩽26
空间优化:转换f[i]时只与前两个元素有关,可将数组定义为只有三个元素的数组。

class Solution {
    public int numDecodings(String s) {
        int n = s.length();
        s = " " + s;
        char[] cs = s.toCharArray();
        int[] dp = new int[3];
        dp[0] = 1;
        for (int i = 1; i <= n; ++i) {
            dp[i % 3] = 0;
            //a代表当前位置单独转码,b代表当前位置和前一个位置共同转码
            int a = cs[i] - '0', b = (cs[i - 1] - '0') * 10 + (cs[i] - '0');
            if (1 <= a && a <= 9) dp[i % 3] = dp[(i - 1) % 3];
            if (10 <= b && b <= 26) dp[i % 3] += dp[(i - 2) % 3];
        }
        return dp[n % 3];
    }
}

执行用时: 2 ms 内存消耗: 36.6 MB

官方题解:

class Solution {
    public int numDecodings(String s) {
        int n = s.length();
        int[] f = new int[n + 1];
        f[0] = 1;
        for (int i = 1; i <= n; ++i) {
            if (s.charAt(i - 1) != '0') {
                f[i] += f[i - 1];
            }
            if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26)) {
                f[i] += f[i - 2];
            }
        }
        return f[n];
    }
}

执行用时: 1 ms 内存消耗: 36.9 MB

  1. 单词拆分
    给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
    说明:
    拆分时可以重复使用字典中的单词。
    你可以假设字典中没有重复的单词。
    示例 1:
    输入: s = “leetcode”, wordDict = [“leet”, “code”]
    输出: true
    解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。

题解:
在这里插入图片描述
最优子结构:枚举 s[0…i-1]中的分割点 j ,看 s[0…j-1]组成的字符串 s1(默认j=0 时s1为空串)和s[j…i−1] 组成的字符串 s2是否都合法,如果两个字符串均合法,那么按照定义s1和 s2拼接成的字符串也同样合法。
边界条件:定义 dp[0]=true 表示空串且合法
状态转移方程:dp[i] = dp[j] && wordDictSet.contains(s.substring(j, i))

public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordDictSet = new HashSet(wordDict);
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++) {
            	//s1是否包含在字典判断dp[j]是否为真可知且s2包含在字典里
                if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

7.5 子序列问题

  1. 最长递增子序列
    给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
    子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
    示例 1:
    输入:nums = [10,9,2,5,3,7,101,18]
    输出:4
    解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

题解:
状态定义:dp[i] 表示以 i 结尾的、最长子序列长度;
最优子结构:对于每一个位置 i,如果其之前的某个位置 j 所对应的数字小于位置 i 所对应的数字,则可以得到一个以 i 结尾的、长度为 dp[j] + 1 的子序列。
状态转移方程:dp[i] = Math.max(dp[i], dp[j] + 1)

class Solution {
    public int lengthOfLIS(int[] nums) {
        int maxLength = 0, n = nums.length;
        if (n <= 1) return n;
        int[] dp = new int[n + 1];
        //所有元素置1,含义是每个元素都至少可以单独成为子序列,此时长度都为1
        Arrays.fill(dp, 1);
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLength = Math.max(maxLength, dp[i]);
        }
        return maxLength;
    }
}

执行用时: 74 ms 内存消耗: 38.1 MB

使用二分查找代替遍历降低时间复杂度:
遍历每一个位置 i,如果其对应的数字大于 dp 数组中所有数字的值,那么我们把它放在 dp 数组尾部,表示最长递增子序列长度加 1;如果这个数字在 dp 数组中比数字 a 大、比数字 b 小,则我们将 b 更新为此数字,使得之后构成递增序列的可能性增大。

class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] tails = new int[nums.length];
        int res = 0;
        for(int num : nums) {
            int i = 0, j = res;
            while(i < j) {
                int m = (i + j) / 2;
                if(tails[m] < num) i = m + 1;
                else j = m;
            }
            tails[i] = num;
            if(res == j) ++res;
        }
        return res;
    }
}

执行用时: 4 ms 内存消耗: 38.1 MB

  1. 最长公共子序列
    给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
    一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
    例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
    两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
    示例 1:
    输入:text1 = “abcde”, text2 = “ace”
    输出:3
    解释:最长公共子序列是 “ace” ,它的长度为 3 。

题解:
状态定义:定义 dp[i][j] 表示 text1[0,i-1] 和 text2[0,j-1] 的最长公共子序列;
穷举分析:
①两个字符串最后一位相等ac和bc,dp[1][1] = dp0][0] + 1;
②两个字符串最后一位不相等ace和bc,dp[2][1] = max(dp[1][1], dp[2][0]),即ace和b的最长子序列长度与ac和bc最长子序列长度的较大值;
状态转移方程:
当 text1[i - 1] == text2[j - 1]时,dp[i][j]=dp[i−1][j−1]+1;
当 text1[i - 1] != text2[j - 1]时,dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
状态初始化:
当 i=0 或 j=0 时 dp[0][j] 或 dp[i][0] 都为0,因此 i 和 j 从1开始遍历;

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m+1][n+1];
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (text1.charAt(i-1) == text2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}

执行用时: 12 ms 内存消耗: 42.2 MB

7.6 背包问题

定义:有N个物品和容量为W的背包,每个物品都有自己的体积w和价值v,求选取哪些物品填充背包可以使背包装下物品的总价值最大。有两种类型:①0-1问题:限定每种物品只能选择0个或1个;②无界或完全背包问题:不限定每种物品的数量。
0-1问题:一个物品只能拿一次或不拿
状态定义: dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值;
穷举分析:遍历第i件物品,此时包的容量为j,有两种情况:①不将物品i放入包中,前i个物品的最大价值等于前i-1个物品的最大价值;②将物品i放入包中,假设物品i的体积为w,价值为v,前i个物品的最大价值为前i-1个物品,体积为j-w的最大价值加i的价值v;
状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1[j-w] + v)
状态初始化:当i=0,j=0时,即不选择,不考虑,i和j从1开始遍历
返回值:i 和 j 取最大值即dp[N][W]

public int knapsack(int[] weights, int[] value, int N, int W) {
	int[][] dp = new int[N+1][W+1];
	for (int i = 1; i <= N; ++i) {
		int w = weights[i-1], v = value[i-1];
		for(int j = 1; j <= W; ++j) {
			if (j >= w) {
				dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w] + v);
			} else {
				dp[i][j] = dp[i-1][j];
			}
		}
	}
	retrun dp[N][W];
} 

时间复杂度:O(NW) 空间复杂度:O(NW)

空间优化:
假设当前遍历物品 i=2,体积为 w=1,v=3,对于当前背包容量j,根据状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)可知dp[2][j]=max(dp[1][j], dp[1][j-1]+3,即 i=2 的最大价值仅与其上一排 i=1 有关,因此可以省略维度 i ,此时状态转移方程变为dp[j] = max(dp[j], dp[j-w] + v)在遍历背包容量 j 时必须使用 --j 逆向遍历数组,从下图可如果正向遍历,遍历到 j 前,dp[j-w]已经更新成 i 的值。
在这里插入图片描述

public int knapsack(int[] weights, int[] values, int N, int W){
	int[] dp = new int[W + 1];
	for (int i = 1; i <= N; ++i) {
		int w = weights[i-1], v = values[i-1];
		for (int j = W, j >= w; --j) {
			dp[j] = Math.max(dp[j], dp[j-w] + v);
		}
	}
	return dp[W];

空间复杂度:O(W)

完全背包问题:一个物品可拿多次
状态定义: dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值;
穷举分析:假设遍历到物品 i=2 ,体积 w=2 ,价值 v=3,此时背包容量 j=5,那么该背包只能容纳最多两个物品 i,有如下三种情况:①不放入物品 i,则dp[2][5] = dp[1][5]; ②放入一个物品 i,dp[2][5] = dp[1][3] + 3; ③放入两个物品 i,dp[2][5] = dp[1][1] + 6.在遍历到dp[2][3]时已经计算过dp[1][3]和dp[2][1],在计算dp[2][1]时也计算过dp[1][1],因此情况②③可以用仅考虑dp[2][3]代替,即dp[2][5] = max(dp[1][5], dp[2][3] + 3;
状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v)
在这里插入图片描述
在这里插入图片描述

public int knapsack(int[] weights, int[] value, int N, int W) {
	int[][] dp = new int[N+1][W+1];
	for (int i = 1; i <= N; ++i) {
		int w = weights[i-1], v = value[i-1];
		for(int j = 1; j <= W; ++j) {
			if (j >= w) {
				dp[i][j] = Math.max(dp[i-1][j], dp[i][j-w] + v);
			} else {
				dp[i][j] = dp[i-1][j];
			}
		}
	}
	retrun dp[N][W];
} 

空间优化:
与0-1背包问题不同,完全背包问题遍历 j 必须正向遍历,因为当前的价值与前面 j-w 列有关。

public int knapsack(int[] weights, int[] values, int N, int W){
	int[] dp = new int[W + 1];
	for (int i = 1; i <= N; ++i) {
		int w = weights[i-1], v = values[i-1];
		for (int j = w, j >= W; ++j) {
			dp[j] = Math.max(dp[j], dp[j-w] + v);
		}
	}
	return dp[W];
  1. 分割等和子集
    给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
    示例 1:
    输入:nums = [1,5,11,5]
    输出:true
    解释:数组可以分割成 [1, 5, 5] 和 [11] 。

题解:
此问题可转化为从输入数组中挑选出一些正整数,使得这些数的和等于整个数组元素的和的一半。
状态定义:dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j;
穷举分析:①选择nums[i],如果在[0,i-1]区间内有一部分元素的和为 j ,则dp[i][j] = true;②不选择nums[i],如果在[0,i-1]区间内有一部分元素和为 j - nums[i],则dp[i][j] = true;
确定边界:当容量j为0s
状态转移方程:dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]]
空间优化:见章首,去掉一个空间

class Solution {
    public boolean canPartition(int[] nums) {
    	//求数组和
        int sum = Arrays.stream(nums).sum();
        //数组和不能平分直接返回false
        if (sum % 2 != 0) return false;
        int target = sum / 2, n = nums.length;
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int i = 1; i <= n; ++i) {
            for (int j = target; j >= nums[i-1]; --j) {
                dp[j] = dp[j] || dp[j-nums[i-1]];
            }
        }
        return dp[target];
    }
}

时间复杂度:O(NC) 其中N是数组元素的个数,C是数组元素和的一半
空间复杂度:O©
执行用时: 22 ms 内存消耗: 37.9 MB

技巧:Java数组求和
int sum = Arrays.stream(nums).sum();

  1. 一和零
    给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
    请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
    如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
    示例 1:
    输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
    输出:4
    解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
    其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

题解:
状态定义:dp[i][j][k]表示输入字符串在[0, i] 区间内能够使用 j 个 0 和 k 个 1 的字符串的最大数量;
穷举分析:当遍历到物品 i 时存在两种情况:①不选择该物品,此时字符串的最大数量与物品 i-1 相同;②选择该物品,此时 j = j - count0 , k = k - count1,即dp[i][j][k] = dp[i-1][j-count0][k-count1] +1 ;
状态转移方程:
dp[i][j][k] = Math.max(dp[i - 1][j][k], dp[i - 1][j - count0][k - count1] + 1)
返回结果:满足题目要求的最大值dp[strs.length][m][n]
空间优化:根据状态转移方程可知当前物品 i 的最大子集数仅与物品 i-1 的最大子集数有关,可省略一层;因为正序遍历使用的上层数据可能以前被之前的遍历所改变,所以需要使用倒序遍历。

public class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m + 1][n + 1];
        dp[0][0] = 0;
        for (String s : strs) {
            int[] count = count(s);
            int count0 = count[0];
            int count1 = count[1];
            for (int i = m; i >= count0; i--) {
                for (int j = n; j >= count1; j--) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - count0][j - count1] + 1);
                }
            }
        }
        return dp[m][n];
    }
    //计算当前字符串中0和1的数量
    private int[] count(String str) {
        int[] res = new int[2];
        for (char c : str.toCharArray()) {
            res[c - '0']++;
        }
        return res;
    }
}

执行用时: 24 ms 内存消耗: 38 MB

  1. 零钱兑换
    给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
    你可以认为每种硬币的数量是无限的。
    示例 1:
    输入:coins = [1, 2, 5], amount = 11
    输出:3
    解释:11 = 5 + 5 + 1

题解:
状态定义:dp[i]为组成金额 i 所需最小的金币数量;
穷举分析:对于当前金额 i ,遍历到金币coin,存在两种情况:①不选择该金币,dp[i] = dp[i] ;②选择该金币,dp[i] = dp[i - coin] + 1;
状态转移方程:dp[i] = Math.min(dp[i], dp[i-coin] + 1)
返回结果:需要判断所需金币数是否大于金额(即金币金额全部为 1 的情况)。

class Solution {
    public int coinChange(int[] coins, int amount) {
        if (coins == null) return -1;
        int[] dp = new int[amount + 1];
        //遍历初始化数组值
        for (int i = 0; i < amount + 1; ++i) {
            dp[i] = amount + 1;
        }
        //数组填充方法
        //Arrays.fill(dp, amount + 1);
        dp[0] = 0;
        for (int i = 1; i <= amount; ++i) {
            for (int coin : coins) {
                if (i >= coin) {
                    dp[i] = Math.min(dp[i], dp[i-coin] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

执行用时: 11ms 内存消耗: 37.9 MB
时间复杂度:O{Sn),S为金额,n为硬币数;
空间复杂度:O(S)

技巧:
使用数组遍历方式初始化数组值相比于Arrays.fill()方法可提升执行时间

7.7 编辑字符串

  1. 编辑距离
    给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
    你可以对一个单词进行如下三种操作:
    插入一个字符
    删除一个字符
    替换一个字符
    示例 1:
    输入:word1 = “horse”, word2 = “ros”
    输出:3
    解释:
    horse -> rorse (将 ‘h’ 替换为 ‘r’)
    rorse -> rose (删除 ‘r’)
    rose -> ros (删除 ‘e’)

题解:
状态定义:dp[i][j] 代表 word1 中前 i 个字符,变换到 word2 中前 j 个字符,所使用的最小操作数;
穷举分析:对于当前字母 i 存在三种操作:①增,在数组中向右遍历,则dp[i][j] = dp[i][j-1] + 1;②减,在数组中向下遍历,则dp[i][j] = dp[i-1][j] + 1;③改,在数组中向右下遍历,则dp[i][j] = dp[i-1][j-1] + 1
在这里插入图片描述

确定边界:i = 0 或 j = 0 属于特殊情况单独考虑;
状态转移方程:dp[i][j] = Math.min(dp[i-1][j-1] + ((word1.charAt(i-1) == word2.charAt(j-1)) ? 0 : 1), Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1));
返回结果:i,j的最大值为m,n,返回值为数组终点即dp[m][n]

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        int[][] dp = new int[m+1][n+1];
        for (int i = 0; i <= m; ++i) {
            for (int j = 0; j <= n; ++j) {
                if (i == 0) {
                    dp[i][j] = j;
                } else if (j == 0) {
                    dp[i][j] = i;
                } else {
                    dp[i][j] = Math.min(dp[i-1][j-1] + ((word1.charAt(i-1) == word2.charAt(j-1)) ? 0 : 1), Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1));
                }
            }
        }
        return dp[m][n];
    }
}

执行用时: 5 ms 内存消耗: 38.4 MB
时间复杂度:O{mn)
空间复杂度:O(mn)

  1. 只有两个键的键盘
    最初在一个记事本上只有一个字符 ‘A’。你每次可以对这个记事本进行两种操作:
    Copy All (复制全部) : 你可以复制这个记事本中的所有字符(部分的复制是不允许的)。
    Paste (粘贴) : 你可以粘贴你上一次复制的字符。
    给定一个数字 n 。你需要使用最少的操作次数,在记事本中打印出恰好 n 个 ‘A’。输出能够打印出 n 个 ‘A’ 的最少操作次数。
    示例 1:
    输入: 3
    输出: 3
    解释:
    最初, 我们只有一个字符 ‘A’。
    第 1 步, 我们使用 Copy All 操作。
    第 2 步, 我们使用 Paste 操作来获得 ‘AA’。
    第 3 步, 我们使用 Paste 操作来获得 ‘AAA’。

题解:
状态定义:dp[i] 表示得到 i 个A需要的最少操作次数;
穷举分析:①如果 n 为质数,则需要copy一次,paste n-1 次,则dp[i] = i ; ②如果 n 为合数,则 n 可以被分解,当 j 可以被 i 整除,该 j 为 i 的一个因子,例:

当 i = 12,
j = 2,12 = 2 * 6,dp[j]=CP dp[i/j]=CPPPPP共8步
j = 3,12 = 3 * 4,dp[j]=CPP dp[i/j]=CPPP共7步
j = 4,12 = 4 * 3,dp[j]=CPPP dp[i/j]=CPP共7步
j = 6,12 = 6 * 2,dp[j]=CPPPPP dp[i/j]=CP共8步

i 与 i / j 交换得到的结果相等,那么 j 的遍历终点可以设置为根号 n ,dp[j]为得到因子 j 需要的操作步数,dp[i/j]为由 j 得到 i 需要的操作步数;
确定边界:i = 1 只有一个字母,需要0次操作,从 2 开始遍历;
状态转移方程:dp[i] = dp[j] + dp[i/j]
返回结果:遍历到 n ,返回为 dp[n] 。

class Solution {
    public int minSteps(int n) {
        int[] dp = new int[n+1];
        //初始化int值 h 为根号 n
        int h = (int) Math.sqrt(n);
        for (int i = 2; i <= n; ++i) {
            dp[i] = i;
            //遍历获得 i 的最大因子 j 
            for (int j = 2; j <= h; ++j) {
            	//如果 i 可以被 j 整除,则因子 j 存在
                if (i % j == 0) {
                    dp[i] = dp[j] + dp[i/j];
                    break;
                }
            }
        }
        return dp[n];
    }
}

执行用时: 1 ms 内存消耗: 35.5 MB

技巧:使用sqrt函数需要转义字符才能赋值给int
int h = (int) Math.sqrt(n);

  1. 正则表达式匹配
    给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘’ 的正则表达式匹配。
    ‘.’ 匹配任意单个字符
    '
    ’ 匹配零个或多个前面的那一个元素
    所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
    示例 1:
    输入:s = “aa” p = “a”
    输出:false
    解释:“a” 无法匹配 “aa” 整个字符串。

题解:
状态定义:

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m+1][n+1];
        dp[0][0] = true;
        for (int i = 1; i < n + 1; ++i) {
            if (p.charAt(i-1) == '*') {
                dp[0][i] = dp[0][i-2];
            }
        }
        for (int i = 1; i < m + 1; ++i) {
            for (int j = 1; j < n + 1; ++j) {
                if (p.charAt(j-1) == '.') {
                    dp[i][j] = dp[i-1][j-1];
                } else if (p.charAt(j-1) != '*') {
                    dp[i][j] = dp[i-1][j-1] && p.charAt(j-1) == s.charAt(i-1);
                } else if (p.charAt(j-2) != s.charAt(i-1) && p.charAt(j-2) != '.') {
                    dp[i][j] = dp[i][j-2];
                } else {
                    dp[i][j] = dp[i][j-1] || dp[i-1][j] || dp[i][j-2];
                }
            }
        }
        return dp[m][n];
    }
}

执行用时: 2 ms 内存消耗: 36.8 MB

7.8 股票交易

股票交易类问题通常可以用动态规划来解决。对于稍微复杂一些的股票交易类问题,比如需
要冷却时间或者交易费用,则可以用通过动态规划实现的状态机来解决。

  1. 买卖股票的最佳时机
    给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
    你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
    返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
    示例 1:
    输入:[7,1,5,3,6,4]
    输出:5
    解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
    注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

题解:

class Solution {
    public int maxProfit(int[] prices) {
        int sell = 0, buy = -2147483648;
        for (int i = 0; i < prices.length; ++i) {
            buy = Math.max(buy, -prices[i]);
            sell = Math.max(sell, buy + prices[i]);
        }
        return sell;
    }
}
  1. 买卖股票的最佳时机 IV
    给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
    设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
    注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
    示例 1:
    输入:k = 2, prices = [2,4,1]
    输出:2
    解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

题解:

class Solution {
    public int maxProfit(int k, int[] prices) {
        int days = prices.length;
        if (days < 2) {
            return 0;
        }
        if (k >= days) {
            return maxProfitUnlimited(prices);
        }
        int[] buy = new int[k + 1];
        for (int i = 0; i < k + 1; ++i) {
            buy[i] = Integer.MIN_VALUE;
        }
        int[] sell = new int[k + 1];
        for (int i = 0; i < days; ++i) {
            for (int j = 1; j <= k; ++j) {
                buy[j] = Math.max(buy[j], sell[j-1] - prices[i]);
                sell[j] = Math.max(sell[j], buy[j] + prices[i]);
            }
        }
        return sell[k];
    } 

    int maxProfitUnlimited(int[] prices) {
        int maxProfit = 0;
        for (int i = 1; i < prices.length; ++i) {
            if (prices[i] > prices[i-1]) {
                maxProfit += prices[i] - prices[i-1];
            }
        }
        return maxProfit;
    }
}

官方题解:

class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length == 0) {
            return 0;
        }

        int n = prices.length;
        k = Math.min(k, n / 2);
        int[] buy = new int[k + 1];
        int[] sell = new int[k + 1];

        buy[0] = -prices[0];
        sell[0] = 0;
        for (int i = 1; i <= k; ++i) {
            buy[i] = sell[i] = Integer.MIN_VALUE / 2;
        }

        for (int i = 1; i < n; ++i) {
            buy[0] = Math.max(buy[0], sell[0] - prices[i]);
            for (int j = 1; j <= k; ++j) {
                buy[j] = Math.max(buy[j], sell[j] - prices[i]);
                sell[j] = Math.max(sell[j], buy[j - 1] + prices[i]);   
            }
        }

        return Arrays.stream(sell).max().getAsInt();
    }
}

技巧:Java数组初始值为0,可以通过遍历的方式改变数组的初始值。
for (int i = 1; i <= k; ++i) { buy[i] = sell[i] = Integer.MIN_VALUE / 2; }

  1. 最佳买卖股票时机含冷冻期
    给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​
    设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
    你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
    卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
    示例:
    输入: [1,2,3,0,2]
    输出: 3
    解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

题解:

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if (n == 0) {
            return 0;
        }
        int[] buy = new int[n];
        int[] sell = new int[n];
        int[] s1 = new int[n];
        int[] s2 = new int[n];
        s1[0] = buy[0] = -prices[0];
        sell[0] = s2[0] = 0;
        for (int i = 1; i < n; i++) {
            buy[i] = s2[i-1] - prices[i];
            s1[i] = Math.max(buy[i-1], s1[i-1]);
            sell[i] = Math.max(buy[i-1], s1[i-1]) + prices[i];
            s2[i] = Math.max(s2[i-1], sell[i-1]);
        }
        return Math.max(sell[n-1], s2[n-1]);
    }
}

官方题解:

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length == 0) {
            return 0;
        }

        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);
    }
}

8 分治问题

8.1 算法概念

概念: 通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。如:归并排序,将大数组分解为小数组再合成一个大数组。

8.2 表达式问题

  1. 为运算表达式设计优先级
    给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。
    示例 1:
    输入: “2-1-1”
    输出: [0, 2]
    解释:
    ((2-1)-1) = 0
    (2-(1-1)) = 2

题解:
利用分治思想

class Solution {
    Map<String, List<Integer>> map = new HashMap();

    public List<Integer> diffWaysToCompute(String input) {
        if (input == null || input.length() <= 0) {
            return new ArrayList<Integer>();
        }
        if (map.containsKey(input)) return map.get(input);
        List<Integer> curRes = new ArrayList<Integer>();
        int length = input.length();
        char[] charArray = input.toCharArray();
        for (int i = 0; i < length; i++) {
            char aChar = charArray[i];
            if (aChar == '+' || aChar == '-' || aChar == '*') { // 当前字符为 操作符
                List<Integer> leftList = diffWaysToCompute(input.substring(0, i));
                List<Integer> rightList = diffWaysToCompute(input.substring(i + 1));
                for (int leftNum : leftList) {
                    for (int rightNum : rightList) {
                        if (aChar == '+') {
                            curRes.add(leftNum + rightNum);
                        } else if (aChar == '-') {
                            curRes.add(leftNum - rightNum);
                        } else {
                            curRes.add(leftNum * rightNum);
                        }
                    }
                }
                
            }
        }

        if (curRes.isEmpty()) {
            curRes.add(Integer.valueOf(input));
        }
        map.put(input, curRes);
        return curRes;
    }
}

8.3 线段树

  1. 最大子序和
    给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
    示例 1:
    输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
    输出:6
    解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

官方题解:

class Solution {
    public class Status {
        public int lSum, rSum, mSum, iSum;

        public Status(int lSum, int rSum, int mSum, int iSum) {
            this.lSum = lSum;
            this.rSum = rSum;
            this.mSum = mSum;
            this.iSum = iSum;
        }
    }

    public int maxSubArray(int[] nums) {
        return getInfo(nums, 0, nums.length - 1).mSum;
    }

    public Status getInfo(int[] a, int l, int r) {
        if (l == r) {
            return new Status(a[l], a[l], a[l], a[l]);
        }
        int m = (l + r) >> 1;
        Status lSub = getInfo(a, l, m);
        Status rSub = getInfo(a, m + 1, r);
        return pushUp(lSub, rSub);
    }

    public Status pushUp(Status l, Status r) {
        int iSum = l.iSum + r.iSum;
        int lSum = Math.max(l.lSum, l.iSum + r.lSum);
        int rSum = Math.max(r.rSum, r.iSum + l.rSum);
        int mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum + r.lSum);
        return new Status(lSum, rSum, mSum, iSum);
    }
}

执行用时: 1 ms 内存消耗: 38.9 MB

9 数学问题

9.1 公倍数与公因数

辗转相除法:

int gcd(int a, int b) {
	return b == 0 ? a : gcd(b, a % b);
}
int lcm(int a, int b) {
	return a * b / gcd(a, b);
}

9.2 质数

质数又称素数,指的是指在大于 1 的自然数中,除了 1 和它本身以外不再有其他因数的自然数。每一个数都可以分解成质数的乘积。

  1. 计数质数
    统计所有小于非负整数 n 的质数的数量。
    示例 1:
    输入:n = 10
    输出:4
    解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。

题解:
埃拉托斯特尼筛法:埃拉托斯特尼筛法,简称埃氏筛或爱氏筛,是一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。要得到自然数 n 以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。

class Solution {
    public int countPrimes(int n) {
        int[] isPrime = new int[n];
        Arrays.fill(isPrime, 1);
        int ans = 0;
        for (int i = 2; i < n; ++i) {
            if (isPrime[i] == 1) {
                ans += 1;
                if ((long) i * i < n) {
                    for (int j = i * i; j < n; j += i) {
                        isPrime[j] = 0;
                    }
                }
            }
        }
        return ans;
    }
}

执行用时: 57 ms 内存消耗: 56.4 MB
时间复杂度:O(nlog logn)
空间复杂度:O(n)

9.3 数字处理

  1. 七进制数
    给定一个整数,将其转化为7进制,并以字符串形式输出。
    示例 1:
    输入: 100
    输出: “202”

题解:

class Solution {
    public String convertToBase7(int num) {
        if (num == 0) return "0";
        boolean isNegative = num < 0;
        if (isNegative) num = -num;
        String res = "";
        while (num != 0) {
            int a = num / 7, b = num % 7;
            res = Integer.toString(b) + res;
            num = a;
        }
        return isNegative ? "-" + res : res;
    }
}

执行用时: 1 ms 内存消耗: 35.7 MB

技巧:int转义字符串使用Integer.toString(int)

  1. 阶乘后的零
    给定一个整数 n,返回 n! 结果尾数中零的数量。
    示例 1:
    输入: 3
    输出: 0
    解释: 3! = 6, 尾数中没有零。

题解:

class Solution {
    public int trailingZeroes(int n) {
        return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
    }
}

执行用时: 0 ms 内存消耗: 35.4 MB

  1. 3的幂
    给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false 。
    整数 n 是 3 的幂次方需满足:存在整数 x 使得 n == 3x
    示例 1:
    输入:n = 27
    输出:true

题解:
对数取模

class Solution {
    public boolean isPowerOfThree(int n) {
        return Math.log10(n) / Math.log10(3) % 1 == 0;
    }
}

执行用时: 16 ms 内存消耗: 38.2 MB

题解:

class Solution {
    public boolean isPowerOfThree(int n) {
        return n > 0 && 1162261467 % n == 0;
    }
}

执行用时: 14 ms 内存消耗: 38.4 MB

  1. 字符串相加
    给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。
    提示:
    1.num1 和num2 的长度都小于 5100
    2.num1 和num2 都只包含数字 0-9
    3.num1 和num2 都不包含任何前导零
    4.你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式

题解:

class Solution {
    public String addStrings(String num1, String num2) {
        StringBuilder res = new StringBuilder("");
        int i = num1.length() - 1, j = num2.length() - 1, carry = 0;
        while(i >= 0 || j >= 0){
            int n1 = i >= 0 ? num1.charAt(i) - '0' : 0;
            int n2 = j >= 0 ? num2.charAt(j) - '0' : 0;
            int tmp = n1 + n2 + carry;
            carry = tmp / 10;
            res.append(tmp % 10);
            i--; j--;
        }
        if(carry == 1) res.append(1);
        return res.reverse().toString();
    }
}

执行用时: 2 ms 内存消耗: 38.2 MB

9.4 随机与取样

  1. 打乱数组
    给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。
    实现 Solution class:
    Solution(int[] nums) 使用整数数组 nums 初始化对象
    int[] reset() 重设数组到它的初始状态并返回
    int[] shuffle() 返回数组随机打乱后的结果
    示例:
    输入
    [“Solution”, “shuffle”, “reset”, “shuffle”]
    [[[1, 2, 3]], [], [], []]
    输出
    [null, [3, 1, 2], [1, 2, 3], [1, 3, 2]]
    解释
    Solution solution = new Solution([1, 2, 3]);
    solution.shuffle(); // 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。例如,返回 [3, 1, 2]
    solution.reset(); // 重设数组到它的初始状态 [1, 2, 3] 。返回 [1, 2, 3]
    solution.shuffle(); // 随机返回数组 [1, 2, 3] 打乱后的结果。例如,返回 [1, 3, 2]

题解:

class Solution {
    private int[] array;
    private int[] original;

    Random rand = new Random();

    private int randRange(int min, int max) {
        return rand.nextInt(max - min) + min;
    }

    private void swapAt(int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    public Solution(int[] nums) {
        array = nums;
        original = nums.clone();
    }
    
    /** Resets the array to its original configuration and return it. */
    public int[] reset() {
        array = original;
        original = original.clone();
        return original;
    }
    
    /** Returns a random shuffling of the array. */
    public int[] shuffle() {
        for (int i = 0; i < array.length; ++i) {
            swapAt(i, randRange(i, array.length));
        }
        return array;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(nums);
 * int[] param_1 = obj.reset();
 * int[] param_2 = obj.shuffle();
 */
  1. 按权重随机选择
    给定一个正整数数组 w ,其中 w[i] 代表下标 i 的权重(下标从 0 开始),请写一个函数 pickIndex ,它可以随机地获取下标 i,选取下标 i 的概率与 w[i] 成正比。
    例如,对于 w = [1, 3],挑选下标 0 的概率为 1 / (1 + 3) = 0.25 (即,25%),而选取下标 1 的概率为 3 / (1 + 3) = 0.75(即,75%)。
    也就是说,选取下标 i 的概率为 w[i] / sum(w) 。
    示例 1:
    输入:
    [“Solution”,“pickIndex”]
    [[[1]],[]]
    输出:
    [null,0]
    解释:
    Solution solution = new Solution([1]);
    solution.pickIndex(); // 返回 0,因为数组中只有一个元素,所以唯一的选择是返回下标 0。

题解:
前缀和+二分查找
概率:
p[取下标 0 ] = p[取范围[0,1)]
p[取下标 1 ] = p[取范围[1,4)]
所以将取下标 i 问题转化为取前缀和的问题,使用二分查找找到最近一个前缀和大于 target 的下标。
在这里插入图片描述

class Solution {
    int[] prefix;//前缀和数组
    int sum = 0;//总和
    Random random = new Random();//生成随机数

    public Solution(int[] w) {
   		//初始化前缀和数组
        int n = w.length;
        prefix = new int[n];
        prefix[0] = w[0];
        //数组求和
        for (int i = 1; i < n; ++i) prefix[i] = prefix[i-1] + w[i];
        sum = prefix[n-1];
    }
    
    public int pickIndex() {
        int target = random.nextInt(sum);
        //二分查找
        int left = 0, right = prefix.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            //区间为左闭右开,等于的情况也要将左指针右移
            if (prefix[mid] <= target) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(w);
 * int param_1 = obj.pickIndex();
 */

执行用时: 23 ms 内存消耗: 43.6 MB

技巧:
不带参数的nextInt()会生成所有有效的整数(包含正数,负数,0);
带参的nextInt(int x)则会生成一个范围在0~x(不包含 x)内的任意正整数

  1. 链表随机节点
    给定一个单链表,随机选择链表的一个节点,并返回相应的节点值。保证每个节点被选的概率一样。
    进阶:
    如果链表十分大且长度未知,如何解决这个问题?你能否使用常数级空间复杂度实现?
    示例:
    // 初始化一个单链表 [1,2,3].
    ListNode head = new ListNode(1);
    head.next = new ListNode(2);
    head.next.next = new ListNode(3);
    Solution solution = new Solution(head);
    // getRandom()方法应随机返回1,2,3中的一个,保证每个元素被返回的概率相等。
    solution.getRandom();

题解:

/**
 * 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 {
	//初始化链表集合 list
    List<Integer> list=new ArrayList<>();
    //生成随机数
    Random random = new Random();
    /** @param head The linked list's head.
        Note that the head is guaranteed to be not null, so it contains at least one node. */
    public Solution(ListNode head) {
        while(head != null){
            list.add(head.val);
            head=head.next;
        }
    }
    
    /** Returns a random node's value. */
    public int getRandom() {
    	//生成范围内的任意正整数
        int n = random.nextInt(list.size());
        return list.get(n);
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(head);
 * int param_1 = obj.getRandom();
 */

执行用时: 9 ms 内存消耗: 39.9 MB

10 位运算

10.1 算法概念

位运算符号:

符号名称
^按位异或
&按位与
|按位或
~取反
<<算术左移
>>算术右移

技巧:
n & (n-1) 删除 n 中 1 的最低位;
n & (-n) 得到 n 中 1 的最低位。

10.2 位运算基础

  1. 汉明距离
    两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
    给你两个整数 x 和 y,计算并返回它们之间的汉明距离。
    示例 1:
    输入:x = 1, y = 4
    输出:2
    解释:
    1 (0 0 0 1)
    4 (0 1 0 0)
    ↑ ↑
    上面的箭头指出了对应二进制位不同的位置。

题解:
移位计数法

class Solution {
    public int hammingDistance(int x, int y) {
        int diff = x ^ y, ans = 0;
        while (diff != 0) {
            ans += diff & 1;
            diff >>= 1;
        }
        return ans;
    }
}

执行用时: 0 ms 内存消耗: 35.2 MB

官方题解:
Brian Kernighan算法

class Solution {
    public int hammingDistance(int x, int y) {
        int s = x ^ y, ret = 0;
        while (s != 0) {
            s &= s - 1;
            ret++;
        }
        return ret;
    }
}
  1. 颠倒二进制位
    颠倒给定的 32 位无符号整数的二进制位。
    提示:
    请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
    在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。
    进阶:
    如果多次调用这个函数,你将如何优化你的算法?
    示例 1:
    输入: 00000010100101000001111010011100
    输出: 00111001011110000010100101000000
    解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
    因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。

题解:
逐位颠倒

public class Solution {
    // you need treat n as an unsigned value
    public int reverseBits(int n) {
        int ans = 0;
        for (int i = 0; i < 32; ++i) {
            ans <<= 1;
            ans += n & 1;
            n >>= 1;
        }
        return ans;
    }
}

执行用时: 1 ms 内存消耗: 38.1 MB

官方题解:
位运算分治

public class Solution {
    private static final int M1 = 0x55555555; // 01010101010101010101010101010101
    private static final int M2 = 0x33333333; // 00110011001100110011001100110011
    private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
    private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111

    public int reverseBits(int n) {
        n = n >>> 1 & M1 | (n & M1) << 1;
        n = n >>> 2 & M2 | (n & M2) << 2;
        n = n >>> 4 & M4 | (n & M4) << 4;
        n = n >>> 8 & M8 | (n & M8) << 8;
        return n >>> 16 | n << 16;
    }
}

执行用时: 1 ms 内存消耗: 38.1 MB

  1. 只出现一次的数字
    给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
    说明:
    你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
    示例 1:
    输入: [2,2,1]
    输出: 1

题解:

class Solution {
    public int singleNumber(int[] nums) {
        int ans = 0;
        for (int num : nums) {
            ans ^= num;
        }
        return ans;
    }
}

执行用时: 1 ms 内存消耗: 38.7 MB
时间复杂度:O(n)
空间复杂度:O(1)

10.3 二进制特性

  1. 4的幂
    给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。
    整数 n 是 4 的幂次方需满足:存在整数 x 使得 n == 4x
    示例 1:
    输入:n = 16
    输出:true

题解:

class Solution {
    public boolean isPowerOfFour(int n) {
        return (n > 0) && ((n & (n-1)) == 0) && (n % 3 == 1);
    }
}

执行用时: 1 ms 内存消耗: 35.2 MB
时间复杂度:O(1)
空间复杂度:O(1)

  1. 最大单词长度乘积
    给定一个字符串数组 words,找到 length(word[i]) * length(word[j]) 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 0。
    示例 1:
    输入: [“abcw”,“baz”,“foo”,“bar”,“xtfn”,“abcdef”]
    输出: 16
    解释: 这两个单词为 “abcw”, “xtfn”。

题解:

class Solution {
    public int maxProduct(String[] words) {
        int res = 0;
        int[] wordsNum = new int[words.length];
        for(int i = 0; i < words.length; i++) {
            String word = words[i];
            int num = 0;
            for(int j = 0; j < word.length(); j++) {
                int wei = word.charAt(j)-'a';
                num = num | (1<<wei);
            }
            wordsNum[i] = num;
        }

        for(int i = 0; i < words.length; i++) {
            for(int j = i+1; j < words.length; j++) {
                if((wordsNum[i]&wordsNum[j]) != 0) continue;
                res = Math.max(res, words[i].length()*words[j].length());
            }
        }

        return res;
    }
}

执行用时: 7 ms 内存消耗: 38.4 MB

  1. 比特位计数
    给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
    示例 1:
    输入: 2
    输出: [0,1,1]
class Solution {
    public int[] countBits(int n) {
        int[] dp = new int[n+1];
        for (int i = 1; i <= n; ++i) {
            dp[i] = dp[i&(i-1)] + 1;
        }
        return dp;
    }
}

执行用时: 2 ms 内存消耗: 42.7 MB

11 数据结构

11.1 Java数据结构

11.2 数组

  1. 存在重复元素
    给定一个整数数组,判断是否存在重复元素。
    如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。
    示例 1:
    输入: [1,2,3,1]
    输出: true

题解:
排序+相邻元素比较

class Solution {
    public boolean containsDuplicate(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        for (int i = 0; i < n - 1; i++) {
            if (nums[i] == nums[i + 1]) {
                return true;
            }
        }
        return false;
    }
}

执行用时: 3 ms 内存消耗: 41.8 MB

  1. 旋转图像
    给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
    你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
    示例 1:
    在这里插入图片描述
    输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
    输出:[[7,4,1],[8,5,2],[9,6,3]]

题解:
空间旋转法,每次只考虑四个间隔90°的位置。
在这里插入图片描述

class Solution {
    public void rotate(int[][] matrix) {
        int temp = 0, n = matrix.length - 1;
        for (int i = 0; i <= n/2; ++i) {
            for (int j = i; j < n-i; ++j) {
            	//右到临时
                temp = matrix[j][n-i];
                //上转到右
                matrix[j][n-i] = matrix[i][j];
                //左转到上
                matrix[i][j] = matrix[n-j][i];
                //下转到左
                matrix[n-j][i] = matrix[n-i][n-j];
                //临时到下
                matrix[n-i][n-j] = temp;
            }
        }
    }
}

执行用时: 0 ms 内存消耗: 38.4 MB
时间复杂度:O(N²)
空间复杂度:O(1)

官方题解:

class Solution {
    public void rotate(int[][] matrix) {
        int n = matrix.length;
        // 水平翻转
        for (int i = 0; i < n / 2; ++i) {
            for (int j = 0; j < n; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - i - 1][j];
                matrix[n - i - 1][j] = temp;
            }
        }
        // 主对角线翻转
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }
    }
}

执行用时: 0 ms 内存消耗: 38.7 MB

  1. 搜索二维矩阵 II
    编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
    每行的元素从左到右升序排列。
    每列的元素从上到下升序排列。
    示例 1:
    在这里插入图片描述
    输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
    输出:true

题解:
利用行和列已经排序号的特性

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length;
        if (m == 0) {
            return false;
        }
        int n = matrix[0].length;
        //从最小行和最大列的元素开始遍历
        int i = 0, j = n - 1;
        while (i < m && j >= 0) {
            if (matrix[i][j] == target) {
                return true;
            //该值大于目标值,减小列
            } else if (matrix[i][j] > target) {
                --j;
            //该值小于目标值,增大行
            } else {
                ++i;
            }
        }
        return false;
    }
}

执行用时: 4 ms 内存消耗: 44 MB
时间复杂度:O(n+m),每次迭代时都是增加 m 行或减小 n 列,所以是常数相加
空间复杂度:O(1),不需要额外空间仅维护两个指针

  1. 最多能完成排序的块
    数组arr是[0, 1, …, arr.length - 1]的一种排列,我们将这个数组分割成几个“块”,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。
    我们最多能将数组分成多少块?
    示例 1:
    输入: arr = [4,3,2,1,0]
    输出: 1
    解释:
    将数组分成2块或者更多块,都无法得到所需的结果。
    例如,分成 [4, 3], [2, 1, 0] 的结果是 [3, 4, 0, 1, 2],这不是有序的数组。

题解:
从左向右遍历,同时记录最大值:
(1)如果当前最大值大于数组位置,说明右侧一定有比当前位置小的数字,重新排列后不会升序,不能分割;
(2)如果当前最大值等于数组位置,说明右侧不再包含小于该位置的数,可分割一次。
在这里插入图片描述

class Solution {
    public int maxChunksToSorted(int[] arr) {
        int chunks = 0, cur = 0;
        //从左向右遍历
        for (int i = 0; i < arr.length; ++i) {
        	//记录最大值
            cur = Math.max(cur, arr[i]);
            //当前最大值与数组位置相等,分割块数加1
            if (cur == i) ++chunks;
        }
        return chunks;
    }
}

执行用时: 0 ms 内存消耗: 35.8 MB
时间复杂度:O(N),N为数组长度需要遍历整个数组
空间复杂度:O(1)固定空间

11.3 栈和队列

  1. 用栈实现队列
    或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
    进阶:
    你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。
    示例:
    输入:
    [“MyQueue”, “push”, “push”, “peek”, “pop”, “empty”]
    [[], [1], [2], [], [], []]
    输出:
    [null, null, null, 1, 1, false]
    解释:
    MyQueue myQueue = new MyQueue();
    myQueue.push(1); // queue is: [1]
    myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
    myQueue.peek(); // return 1
    myQueue.pop(); // return 1, queue is [2]
    myQueue.empty(); // return false
    提示:
    1 <= x <= 9
    最多调用 100 次 push、pop、peek 和 empty
    假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

题解:
用两个栈实现一个队列,通过额外栈反转数组。
一个栈表示队列输入,用于压入 push 操作;
一个栈表示队列输出,用于弹出 pop 和查看堆顶 peek。
在这里插入图片描述

class MyQueue {
    Stack<Integer> in = new Stack<Integer>();
    Stack<Integer> out = new Stack<Integer>();

    /** Initialize your data structure here. */
    public MyQueue() {

    }
    
    /** Push element x to the back of queue. */
    public void push(int x) {
        in.push(x);
    }
    
    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        in2Out();
        int x = out.peek();
        out.pop();
        return x;
    }
    
    /** Get the front element. */
    public int peek() {
        in2Out();
        return out.peek();
    }
    //反转
    void in2Out() {
        if (out.empty()) {
            while (!in.empty()) {
                int x = in.peek();
                in.pop();
                out.push(x);
            }
        }
    }

    /** Returns whether the queue is empty. */
    public boolean empty() {
        return in.empty() && out.empty();
    }
}

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * boolean param_4 = obj.empty();
 */

执行用时: 0 ms 内存消耗: 36.2 MB
时间复杂度:O(1),push 和 empty 为 1 ,pop he peek 均摊复杂度为 1;
空间复杂度:O(n),n 为操作数。对于有 n 次 push 操作的情况,队列中会有 n 个元素。

  1. 最小栈
    设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
    push(x) —— 将元素 x 推入栈中。
    pop() —— 删除栈顶的元素。
    top() —— 获取栈顶元素。
    getMin() —— 检索栈中的最小元素。
    示例:
    输入:
    [“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
    [[],[-2],[0],[-3],[],[],[],[]]
    输出:
    [null,null,null,null,-3,null,0,-2]
    解释:
    MinStack minStack = new MinStack();
    minStack.push(-2);
    minStack.push(0);
    minStack.push(-3);
    minStack.getMin(); --> 返回 -3.
    minStack.pop();
    minStack.top(); --> 返回 0.
    minStack.getMin(); --> 返回 -2.
    提示:
    pop、top 和 getMin 操作总是在 非空栈 上调用。

题解:
辅助栈:数值栈+最小值栈
在这里插入图片描述

class MinStack {
    private Stack<Integer> s;
    private Stack<Integer> min_s;

    /** initialize your data structure here. */
    public MinStack() {
        s = new Stack<>();
        min_s = new Stack<>();
    }
    
    public void push(int val) {
        s.push(val);
        if (min_s.empty() || min_s.peek() >= val) {
            min_s.push(val);
        }
    }
    //泛型类型比较,不能用==
    public void pop() {
        if (s.pop().equals(min_s.peek())) {
            min_s.pop();
        }
    }
    
    public int top() {
        return s.peek();
    }
    
    public int getMin() {
        return min_s.peek();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

执行用时: 6 ms 内存消耗: 40.4 MB
时间复杂度:O(1),push、pop和getMin的最小时间复杂度为O(1)
空间复杂度:O(n),含 n 个元素的辅助栈的额外空间

技巧:
s.pop().equals(min_s.peek())
泛型类型比较不能用 == ,要用 equals

题解:
数组栈:数值和最小值放入一个数组中

class MinStack {
    private Stack<int[]> s = new Stack<>();

    /** initialize your data structure here. */
    public MinStack() {}
    
    public void push(int x) {
        if (s.isEmpty()) {
            s.push(new int[]{x, x});
        } else {
            s.push(new int[]{x, Math.min(x, s.peek()[1])});
        }
    }
    
    public void pop() {
        s.pop();
    }
    
    public int top() {
        return s.peek()[0];
    }
    
    public int getMin() {
        return s.peek()[1];
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

执行用时: 4 ms 内存消耗: 40.3 MB

题解:
链表模拟栈:数值与最小值绑定放入链表中
在这里插入图片描述

class MinStack {
    // 链表模拟栈
    private Node node;

    // 内部类,栈中存储的就是一个个Node
    // 每个Node包含其元素本身的大小,同时还包含当前栈中最小的值,还有其先驱节点
    private class Node {
        int val;
        int min;
        Node prev;

        private Node(int val, int min){
            this.val = val;
            this.min = min;
        }

        private Node(int val, int min, Node prev){
            this.val = val;
            this.min = min;
            this.prev = prev;
        }
    }
    
    public MinStack() {

    }

    // 添加元素
    public void push(int x) {
        // 没有时,新建头结点
        if (node == null){
            node = new Node(x, x);
        }
        else {
            // 新建节点
            Node next = new Node(x, Math.min(x, node.min));
            // 指示节点后移
            next.prev = node;
            node = next;
        }
    }

    // 反正不要返回结果,直接将指示节点前移
    public void pop() {
        node = node.prev;
    }

    // 返回指示节点的值
    public int top() {
        return node.val;
    }

    // 返回指示节点中记录的最小值
    public int getMin() {
        return node.min;
    }
}

执行用时: 5 ms 内存消耗: 40.3 MB

  1. 有效的括号
    给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
    有效字符串需满足:
    左括号必须用相同类型的右括号闭合。
    左括号必须以正确的顺序闭合。
    示例 1:
    输入:s = “()”
    输出:true

题解:
遍历+栈
字符转换成char数组并遍历:
①遍历到左括号时,右括号入栈;
②遍历到右括号时,与栈顶对比是否相等,不相等返回 false,若栈为空也返回 false;
遍历完后栈 stack 为空,则返回true

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<Character>();
        for(char c: s.toCharArray()){
            if(c=='(') stack.push(')');
            else if(c=='[') stack.push(']');
            else if(c=='{') stack.push('}');
            else if(stack.isEmpty() || c != stack.pop()) return false;
        }
        return stack.isEmpty(); 
    }
}

执行用时: 1 ms 内存消耗: 36.1 MB

题解:
HashMap+链表
创建哈希表字典,key 为左括号, value 为右括号;创建链表 stack ,stack 不能弹出空,因此默认初始化加入一个 ? ;
遍历字符串转换成的 char 数组:
①遍历到左括号时即map.containsKey(c),stack 加入该字符;
②遍历到右括号时,map.get(stack.removeLast()与栈顶对比是否相等,不相等返回 false;
返回结果stack.size() == 1,只有 ?结果为true,否则还有剩余左括号返回 false。

class Solution {
    private static final Map<Character,Character> map = new HashMap<Character,Character>(){{
        put('{','}'); put('[',']'); put('(',')'); put('?','?');
    }};
    public boolean isValid(String s) {
        if(s.length() > 0 && !map.containsKey(s.charAt(0))) return false;
        LinkedList<Character> stack = new LinkedList<Character>() {{ add('?'); }};
        for(Character c : s.toCharArray()){
            if(map.containsKey(c)) stack.addLast(c);
            else if(map.get(stack.removeLast()) != c) return false;
        }
        return stack.size() == 1;
    }
}

执行用时: 1 ms 内存消耗: 36.4 MB

11.4 单调栈

  1. 每日温度
    请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
    示例 1:
    输入: temperatures = [73,74,75,71,69,72,76,73]
    输出: [1,1,4,2,1,1,0,0]

题解:
单调递减栈indices:存放温度的位置,表示日期,即数组的序号;
数组ans:储存日期差值
遍历数组,对于当前日期i与当前栈顶序号代表的温度进行比较①如果i的温度比栈顶pre_index的温度高,则取出栈顶indices.pop(),并记录当前日期的差值ans[pre_index] = i - pre_index;②如果i的温度比栈顶pre_index的温度低,则将i插入栈顶。栈内数组永远单调递减,避免使用排序进行比较。

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        int[] ans = new int[n];
        Stack<Integer> indices = new Stack<Integer>();
        for (int i = 0; i < n; ++i) {
            while (!indices.empty()) {
                int pre_index = indices.peek();
                //当前温度与栈顶代表温度比较
                if (temperatures[i] <= temperatures[pre_index]) break;
                //栈顶弹出
                indices.pop();
                ans[pre_index] = i - pre_index;
            }
            //当前日期入栈
            indices.push(i);
        }
        return ans;
    }
}

执行用时: 32 ms 内存消耗: 47.4 MB
时间复杂度:O(n)
空间复杂度:O(n)

11.5 优先队列

在这里插入图片描述
优先队列(PriorityQueue)是一个基于优先级的无界优先级队列,可以在 O(1) 时间内获得最大值,并且可以在 O(log n) 时间内取出最大值或插入任意值。
在优先级队列中,添加的对象根据其优先级。默认情况下,优先级由对象的自然顺序决定。队列构建时提供的比较器可以覆盖默认优先级:
在这里插入图片描述

详解优先队列

  1. 合并K个升序链表
    给你一个链表数组,每个链表都已经按升序排列。
    请你将所有链表合并到一个升序链表中,返回合并后的链表。
    示例 1:
    输入:lists = [[1,4,5],[1,3,4],[2,6]]
    输出:[1,1,2,3,4,4,5,6]
    解释:链表数组如下:
    [
    1->4->5,
    1->3->4,
    2->6
    ]
    将它们合并到一个有序链表中得到。
    1->1->2->3->4->4->5->6

题解:
比较k个节点(即每个链表的首节点),获得最小值的节点,将选中的节点放在最终有序链表的后端,使用优先队列可优化这个比较获取最小值的过程。
head : 保存合并之后的链表的头部;
tail : 记录下一个插入位置的前一个位置
在这里插入图片描述

/**
 * 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) {
    	//声明优先队列,返回两个节点比较
        PriorityQueue<ListNode> q = new PriorityQueue<>((x,y) -> x.val - y.val);
        //将输入节点放入优先队列
        for (ListNode node : lists) {
            if (node != null) {
                q.add(node);
            }
        }
        //声明头节点和构建链表节点
        ListNode head = new ListNode();
        ListNode tail = head;
        //循环迭代队列,每次取出最小节点放入有序链表尾部
        while (!q.isEmpty()) {
            tail.next = q.poll();
            //将指针放入下一个节点
            tail = tail.next;
            //如果下一个节点不为空则将下一个节点放入队列中
            if (tail.next != null) {
                q.add(tail.next);
            }
        }
        //返回头节点的下一个节点
        return head.next;
    }
}

执行用时: 4 ms 内存消耗: 39.9 MB
时间复杂度:O(Nlogk), k 是链表的数目,N 是节点的数目;
空间复杂度:O(n) 创新一个新的链表的开销

  1. 天际线问题
    城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线 。
    每个建筑物的几何信息由数组 buildings 表示,其中三元组 buildings[i] = [lefti, righti, heighti] 表示:
    lefti 是第 i 座建筑物左边缘的 x 坐标。
    righti 是第 i 座建筑物右边缘的 x 坐标。
    heighti 是第 i 座建筑物的高度。
    天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],…] ,并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y 坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
    注意:输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12 7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]
    示例 1:
    在这里插入图片描述
    输入:buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
    输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
    解释:
    图 A 显示输入的所有建筑物的位置和高度,
    图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。

题解:
优先队列+哈希表延迟删除
关键点选取规则:
如果是「从下到上」转向「水平方向」,纵坐标最大的点是关键点;
如果是「从上到下」转向「水平方向」,纵坐标第二大的点是关键点。
在这里插入图片描述
第一步:遍历建筑,将左右边缘转折点加入到数组buildingPoints中,将左边缘的高度处理成负数再加入数组中,将数组buildingPoints按照横坐标排序;
第二步:遍历边缘点,高度为负数即左边缘加入优先队列,右边缘加入到延迟删除哈希表delayed中;
第三步:通过高度差来判断当前转折点是否符合关键点选取规则,如果当前高度curheight与前一个高度不相等,则次转折点符合规则,curHeight为返回点的纵坐标,其对应的buildingPoints[0]为横坐标。

在这里插入图片描述

class Solution {
    public List<List<Integer>> getSkyline(int[][] buildings) {
    	//新建储存端点的List
        List<int[]> buildingPoints = new ArrayList<>();
        for (int[] b : buildings) {
        	//左端点,纵坐标取负数做区分
            buildingPoints.add(new int[]{b[0], -b[2]});
            //右端点
            buildingPoints.add(new int[]{b[1], b[2]});
        }
		//按照横坐标排序,横坐标相同时高度高的排在前面
        buildingPoints.sort((a, b) -> {
            return a[0] != b[0] ? a[0] - b[0] : a[1] - b[1];
        });

        PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(Collections.reverseOrder());
        //哈希表记录「延迟删除」的元素,key 为元素,value 为需要删除的次数
        Map<Integer, Integer> delayed = new HashMap<>();
        //为计算高度差初始化一个宽高为0的建筑
        maxHeap.offer(0);
        //保存之前的最高高度,初始化为0
        int lastHeight = 0;
        List<List<Integer>> res = new ArrayList<>();
        for (int[] buildingPoint : buildingPoints) {
            if (buildingPoint[1] < 0) {
            	//把负数高度翻转并加入优先队列
                maxHeap.offer(-buildingPoint[1]);
            } else {
            	//不是真的删除 buildingPoint[1],把它放进 delayed,等到堆顶元素是 buildingPoint[1] 的时候,才真的删除
                delayed.put(buildingPoint[1], delayed.getOrDefault(buildingPoint[1], 0) + 1);
            }
            // 如果堆顶元素在延迟删除集合中,才真正删除,这一步可能执行多次,所以放在 while 中
            // while (true) 都是可以的,因为 maxHeap 一定不会为空
            while (!maxHeap.isEmpty()) {
                int curHeight = maxHeap.peek();
                if (delayed.containsKey(curHeight)) {
                    delayed.put(curHeight, delayed.get(curHeight) - 1);
                    if (delayed.get(curHeight) == 0) {
                        delayed.remove(curHeight);
                    }
                    maxHeap.poll();
                } else {
                    break;
                }
            }
            int curHeight = maxHeap.peek();
            //当前高度与前一个高度不相等产生高度差,此时该转折点符合要求是一个关键点
            if (curHeight != lastHeight) {
                res.add(Arrays.asList(buildingPoint[0], curHeight));
                //改变高度
                lastHeight = curHeight;
            }
        }
        return res;
    }
}

执行用时: 26 ms 内存消耗: 42.4 MB
时间复杂度:O(NlogN)

技巧:延迟删除

官方题解:
扫描线+优先队列
关键点选取规则:
横坐标:每一座建筑物的边缘;
纵坐标:包含该横坐标的所有建筑物的最大高度,建筑的左边缘小于等于该横坐标,右边缘大于该横坐标(即不包含该右边缘)
在这里插入图片描述
第一步:初始化优先队列pq,储存边缘的数组boundaries并将数组排序;
第二步:把符合选取规则的边缘点加入到优先队列,确定关键点的横坐标;
第三步:新建返回数组ret,根据高度判断当前边缘点是否符合选取规则,把符合规则的边缘点加入返回数组。
在这里插入图片描述

class Solution {
    public List<List<Integer>> getSkyline(int[][] buildings) {
    	//使用延迟队列寻找最大高度
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> b[1] - a[1]);
        //数组保存所有的边缘
        List<Integer> boundaries = new ArrayList<Integer>();
        //遍历建筑物并把边缘加入到边缘横坐标加入到数组 boundaries 中
        for (int[] building : buildings) {
        	//加入左边缘
            boundaries.add(building[0]);
            //加入右边缘
            boundaries.add(building[1]);
        }
        //将数组 boundaries 排序
        Collections.sort(boundaries);

		//创建储存返回结果的List
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        //初始化建筑横坐标
        int n = buildings.length, idx = 0;
        //遍历边缘数组中的边缘点
        for (int boundary : boundaries) {
        	//判断建筑是否符合选取规则
            while (idx < n && buildings[idx][0] <= boundary) {
            	//将符合选取规则的建筑加入到队列中
                pq.offer(new int[]{buildings[idx][1], buildings[idx][2]});
                idx++;
            }
            //队首元素不符合关键点选取规则
            while (!pq.isEmpty() && pq.peek()[0] <= boundary) {
            	//队首元素弹出队列
                pq.poll();
            }

			//maxn 记录最大高度,当优先队列为空最大高度为0,否则最大高度为队首元素
            int maxn = pq.isEmpty() ? 0 : pq.peek()[1];
            //如果当前关键点的高度 maxn 与前一个前一个关键点ret.get(ret.size() - 1)的高度不相等时,将该关键点加入返回的ret
            if (ret.size() == 0 || maxn != ret.get(ret.size() - 1).get(1)) {
                ret.add(Arrays.asList(boundary, maxn));
            }
        }
        return ret;
    }
}

执行用时: 17 ms 内存消耗: 41.4 MB
时间复杂度:O(NlogN),N为建筑数量,优先队列入队出队复杂度为logN;
空间复杂度:O(N),N 为建筑数量,边缘数组和优先队列空间占用均为 N

11.6 双端队列

  1. 滑动窗口最大值
    给你一个整数数组 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

题解:
在这里插入图片描述

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 窗口个数
        int[] res = new int[nums.length - k + 1];
        LinkedList<Integer> queue = new LinkedList<>();

        // 遍历数组中元素,right表示滑动窗口右边界
        for(int right = 0; right < nums.length; right++) {
            // 如果队列不为空且当前考察元素大于等于队尾元素,则将队尾元素移除。
            // 直到,队列为空或当前考察元素小于新的队尾元素
            while (!queue.isEmpty() && nums[right] >= nums[queue.peekLast()]) {
                queue.removeLast();
            }

            // 存储元素下标
            queue.addLast(right);

            // 计算窗口左侧边界
            int left = right - k +1;
            // 当队首元素的下标小于滑动窗口左侧边界left时
            // 表示队首元素已经不再滑动窗口内,因此将其从队首移除
            if (queue.peekFirst() < left) {
                queue.removeFirst();
            }

            // 由于数组下标从0开始,因此当窗口右边界right+1大于等于窗口大小k时
            // 意味着窗口形成。此时,队首元素就是该窗口内的最大值
            if (right +1 >= k) {
                res[left] = nums[queue.peekFirst()];
            }
        }
        return res;
    }
}

执行用时: 28 ms 内存消耗: 52.5 MB

官方题解:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
            public int compare(int[] pair1, int[] pair2) {
                return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
            }
        });
        for (int i = 0; i < k; ++i) {
            pq.offer(new int[]{nums[i], i});
        }
        int[] ans = new int[n - k + 1];
        ans[0] = pq.peek()[0];
        for (int i = k; i < n; ++i) {
            pq.offer(new int[]{nums[i], i});
            while (pq.peek()[1] <= i - k) {
                pq.poll();
            }
            ans[i - k + 1] = pq.peek()[0];
        }
        return ans;
    }
}

执行用时: 76 ms 内存消耗: 58.8 MB

11.7 ArrayList和LinkedList

  1. 找到所有数组中消失的数字
    给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
    示例 1:
    输入:nums = [4,3,2,7,8,2,3,1]
    输出:[5,6]

题解:

class Solution {
    public List<Integer> findDisappearedNumbers(int[] nums) {
        LinkedList<Integer> list = new LinkedList<>();
        for(int i = 0; i < nums.length; ++i){
        //把相应的nums中的数对应的-1下标位置,乘以-1 表明这个下标也就是这个数出现过了
            if(nums[Math.abs(nums[i]) - 1] > 0)
                nums[Math.abs(nums[i]) - 1] *= -1; 
        }
        for(int i = 0; i < nums.length; ++i){
            if(nums[i] > 0)
                list.add(i+1);
        }
        return list;
    }
}

11.8 Hash表

  1. 存在重复元素
    给定一个整数数组,判断是否存在重复元素。
    如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。
    示例 1:
    输入: [1,2,3,1]
    输出: true

题解:
将数组中的每个元素插入到哈希表中,如果该元素已经存在于哈希表中,则存在重复元素,返回true。

class Solution {
    public boolean containsDuplicate(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
            if (set.contains(num)) return true;
            set.add(num);
        }
        return false;
    }
}

执行用时: 8 ms 内存消耗: 44.2 MB
时间复杂度:O(N)
空间复杂度:O(N)

  1. 两数之和
    给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
    你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
    你可以按任意顺序返回答案。
    示例 1:
    输入:nums = [2,7,11,15], target = 9
    输出:[0,1]
    解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

题解:
创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,保证不会让 x 和自己匹配。通过哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i< nums.length; i++) {
            if(map.containsKey(target - nums[i])) {
                return new int[] {map.get(target-nums[i]),i};
            }
            map.put(nums[i], i);
        }
        throw new IllegalArgumentException("No two sum solution");
    }
}

执行用时: 1 ms 内存消耗: 38.6 MB
时间复杂度:O(N)
空间复杂度:O(N)

  1. 最长连续序列
    给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
    请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
    示例 1:
    输入:nums = [100,4,200,1,3,2]
    输出:4
    解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

题解:
首先把所有数字放入一个哈希表,然后不断从哈希表中任取一值并删除其前后的所有连续数字,最后更新目前的最长连续序列长度,重复上述过程,找到所有连续数字序列。
在这里插入图片描述

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> hash = new HashSet<Integer>();
        for(int x : nums) hash.add(x);    //放入hash表中
        int res = 0;
        for(int x : hash)
        {
            if(!hash.contains(x-1))
            {
                int y = x;   //以当前数x向后枚举
                while(hash.contains(y + 1)) y++;
                res = Math.max(res, y - x + 1);  //更新答案
            }
        }
        return res;
    }
}

执行用时: 14 ms 内存消耗: 53.2 MB
时间复杂度:O(N),N为数组长度,需要遍历数组中的元素,判断hash表中是否存在y的复杂度为O(1);
空间复杂度:O(N)哈希表维护的空间

  1. 直线上最多的点数
    给你一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。
    示例 1:
    在这里插入图片描述
    输入:points = [[1,1],[2,2],[3,3]]
    输出:3

题解:
直线公式:y = kx + b,一直斜率 k 和一点(x0,y0)即可唯一确认该直线;
对于数组中的每个点,对其他点建立哈希表,统计同一斜率的点一共存在多少,同时考虑斜率不存在的直线。
在这里插入图片描述

class Solution {
    public int maxPoints(int[][] points) {
    	//新建哈希表,key 为斜率,value 为点的个数
        HashMap<Double, Integer> hash = new HashMap<>();
        初始化点最大个数
        int max_count = 1;
        //遍历数组中的点
        for (int i = 0; i < points.length; ++i) {
        	//遍历 i 之后的点
            for (int j = i + 1; j < points.length; ++j) {
            	//横坐标差值和纵坐标差值
                double dx = points[i][0] - points[j][0], dy = points[i][1] - points[j][1];
                //直线斜率,特别考虑水平直线和竖直直线两种情况,Java除法 0 和 infinity 有正负区别,正负两种斜率属于同一条直线要特别处理
                double slope = (points[i][0] == points[j][0] && points[i][1] < points[j][1]) || (points[i][1] == points[j][1] && points[i][0] < points[j][0])? -dy/dx : dy/dx;
                //先对当前斜率判空,判断哈希表中是否存在该key
                int count = hash.containsKey(slope) ? hash.get(slope) : 1;
                //对应的 value 加1
                hash.put(slope, count + 1);
            }
            //遍历HashMap中的 key
            for (Double item : hash.keySet()) {
                max_count = Math.max(max_count, hash.get(item));
            }
            //清除哈希表
            hash.clear();
        }
        return max_count;
    }
}

执行用时: 8 ms 内存消耗: 38 MB
时间复杂度:O(n² × logm),其中 n 为点的个数,m 为最大横纵坐标的差值;
空间复杂度:O(n),n 为点的个数,为哈希表占用的空间。

技巧:
(1)HashMap中的 value 自增
int count = hash.containsKey(slope) ? hash.get(slope) : 0; hash.put(slope, count + 1);
(2)Java除法分母为 0 ,分子为正数,结果为 infinity,分子为负数,结果为 -infinity

官方题解:

class Solution {
    public int maxPoints(int[][] points) {
        int n = points.length;
        if (n <= 2) {
            return n;
        }
        int ret = 0;
        for (int i = 0; i < n; i++) {
            if (ret >= n - i || ret > n / 2) {
                break;
            }
            Map<Integer, Integer> map = new HashMap<Integer, Integer>();
            for (int j = i + 1; j < n; j++) {
                int x = points[i][0] - points[j][0];
                int y = points[i][1] - points[j][1];
                if (x == 0) {
                    y = 1;
                } else if (y == 0) {
                    x = 1;
                } else {
                    if (y < 0) {
                        x = -x;
                        y = -y;
                    }
                    int gcdXY = gcd(Math.abs(x), Math.abs(y));
                    x /= gcdXY;
                    y /= gcdXY;
                }
                int key = y + x * 20001;
                map.put(key, map.getOrDefault(key, 0) + 1);
            }
            int maxn = 0;
            for (Map.Entry<Integer, Integer> entry: map.entrySet()) {
                int num = entry.getValue();
                maxn = Math.max(maxn, num + 1);
            }
            ret = Math.max(ret, maxn);
        }
        return ret;
    }

    public int gcd(int a, int b) {
        return b != 0 ? gcd(b, a % b) : a;
    }
}

执行用时: 10 ms 内存消耗: 37.9 MB

11.9 前缀和与积分图

一维前缀和:
一个一维数组 x x x 和该数组的一维前缀和数组 y y y ,则 x x x 和 y y y 满足以下关系:
y 0 = x 0 、 y 1 = x 0 + x 1 、 y 2 = x 0 + x 1 + x 2 、 y n = x 0 + x 1 + x 2 + . . . + x n y_0 = x_0、y_1 = x_0 + x_1、y_2 = x_0 + x_1 + x_2、y_n = x_0 + x_1 + x_2 + ... + x_n y0​=x0​、y1​=x0​+x1​、y2​=x0​+x1​+x2​、yn​=x0​+x1​+x2​+...+xn​
代码实现:
y n = y n − 1 + x n y_n = y_{n-1} + x_n yn​=yn−1​+xn​

for (int i = 0; i < n; i++) {
    if (i == 0) y[i] = x[i];
    else y[i] = y[i-1] + x[i];
}

二维前缀和(积分图):
b 0 , 0 = a 0 , 0 、 b 0 , 1 = a 0 , 0 + a 0 , 1 、 b 1 , 0 = a 0 , 0 + a 1 , 0 、 b 1 , 1 = a 0 , 0 + a 0 , 1 + a 1 , 0 + a 1 , 1 b_{0,0} = a_{0,0}、b_0,_1 = a_0,_0 + a_0,_1、b_1,_0 = a_0,_0 + a_1,_0、b_1,_1 = a_0,_0 + a_0,_1 + a_1,_0 + a_1,_1 b0,0​=a0,0​、b0​,1​=a0​,0​+a0​,1​、b1​,0​=a0​,0​+a1​,0​、b1​,1​=a0​,0​+a0​,1​+a1​,0​+a1​,1​
示例:
在这里插入图片描述
代码实现:
二维前缀和 = 矩阵内值和
b x , y = b x − 1 , y + b x , y − 1 − b x − 1 , y + a x , y b_{x,y} = b_{x-1,y} + b_{x,y-1} - b_{x-1,y} + a_{x,y} bx,y​=bx−1,y​+bx,y−1​−bx−1,y​+ax,y​

for (int y=0; y<n; y++) {//n行
    for (int x=0; x<m; x++){//m行
        if (x==0 && y==0) b[y][x]=a[y][x];//左上角的值
        else if (x==0) b[y][x] = b[y-1][x] + a[y][x];//第一列
        else if (y==0) b[y][x] = b[y][x-1] + a[y][x];//第一行
        else b[y][x] = b[y-1][x] + b[y][x-1] -b[y-1][x-1] + a[y][x];
    }
}
  1. 区域和检索 - 数组不可变
    给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。
    实现 NumArray 类:
    NumArray(int[] nums) 使用数组 nums 初始化对象
    int sumRange(int i, int j) 返回数组 nums 从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点(也就是 sum(nums[i], nums[i + 1], … , nums[j]))
    示例:
    输入:
    [“NumArray”, “sumRange”, “sumRange”, “sumRange”]
    [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
    输出:
    [null, 1, -1, -3]
    解释:
    NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
    numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
    numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
    numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

题解:
sums[i]:第 0 项到第 i 项的和,即

sums[i] = nums[0] + nums[1] + ... + nums[i]

两个相邻前缀和的差与数组 nums 中某项相等,即

nums[i] = sums[i] - sums[i - 1]

则数组 nums 中 i 到 j 的元素和为

nums[i] + ... + nums[j] = sum[j] - nums[i - 1]

为方便计算,不考虑 i=0 的情况,将数组 sum 长度设为 n + 1,则 sum[i] 表示 0 到 i-1 的前缀和

nums[i] + ... + nums[j] = sum[j + 1] - nums[i]
class NumArray {
	//前缀和数组
    int[] sums;

    public NumArray(int[] nums) {
        int n = nums.length;
        sums = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            sums[i + 1] = sums[i] + nums[i];
        }
    }
    
    public int sumRange(int left, int right) {
        return sums[right + 1] - sums[left];
    }
}

执行用时: 7 ms 内存消耗: 41.2 MB
时间复杂度:初始化O(n),n是数组 nums 的长度,初始化需要遍历数组计算前缀和,
每次检索O(1),每次得到两个下标的前缀和并计算差值;
空间复杂度:O(n),n 是数组 nums 的长度。

  1. 二维区域和检索 - 矩阵不可变
    给定一个二维矩阵 matrix,以下类型的多个请求:
    计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2) 。
    实现 NumMatrix 类:
    NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
    int sumRegion(int row1, int col1, int row2, int col2) 返回左上角 (row1, col1) 、右下角 (row2, col2) 的子矩阵的元素总和。
    示例 1:
    在这里插入图片描述
    输入:
    [“NumMatrix”,“sumRegion”,“sumRegion”,“sumRegion”]
    [[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
    输出:
    [null, 8, 11, 12]
    解释:
    NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]]);
    numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和)
    numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和)
    numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)

题解:
二维前缀和(积分图)
sums[i][j] = matrix[i-1][j-1] + sums[i-1][j] + sums[i][j-1] - sums[i-1][j-1]
在这里插入图片描述

class NumMatrix {
    int[][] sums;

    public NumMatrix(int[][] matrix) {
        int m = matrix.length, n = m > 0 ? matrix[0].length : 0;
        sums = new int[m + 1][n + 1];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1];
    }
}

执行用时: 137 ms 内存消耗: 63 MB
时间复杂度:
初始化O(mn),m 和 n 为矩阵 matrix 的行数和列数,遍历矩阵计算前缀和;
每次检索O(1);
空间复杂度:O(mn),m 和 n 为矩阵 matrix 的行数和列数,需要创建一个 m+1 行 n +1 列的矩阵。

  1. 和为K的子数组
    给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
    示例 1 :
    输入:nums = [1,1,1], k = 2
    输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。

题解:
前缀和+哈希表
初始化哈希表,key 为前缀和,value 为该前缀和出现的次数;
假设 psum[i] 为 [0,i] 的前缀和,可得:

psum[i] == psum[i-1] + nums[i]

则 [j,i] 区间内数组和为 k 可得:

psum[i] - psum[j-1] == k
class Solution {
    public int subarraySum(int[] nums, int k) {
    	//count 为前缀和出现次数,psum 为当前前缀和
        int count = 0, psum = 0;
        Map<Integer, Integer> hashmap = new HashMap<>();
        hashmap.put(0, 1);//初始化
        for (int i : nums) {
        	//遍历数组并获得前缀和
            psum += i;
            count = hashmap.containsKey(psum-k) ? hashmap.get(psum - k) + count : count;
            //当前前缀和数量增加 1
            int cnt = hashmap.containsKey(psum) ? hashmap.get(psum) : 0;
            hashmap.put(psum, cnt + 1);
        }
        return count;
    }
}

执行用时: 21 ms 内存消耗: 40.7 MB
时间复杂度:O(N),n 为数组长度,遍历数组的时间复杂度为 O(n),哈希表删除操作时间复杂度为O(1)
空间复杂度:O(N),n 为数组长度,哈希表最坏情况下每个键值都不同,因此需要和数组长度相同的空间。

12 字符串

12.1 字符串基本语法

操作字符数组字符串
声明char[] aString s
根据索引访问字符a[i]s.charAt(i)
获取字符串长度a.lengths.length()
表示方法转换a = s.toCharArray();s = new String(a);

12.2 字符串比较

  1. 有效的字母异位词
    给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
    注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
    示例 1:
    输入: s = “anagram”, t = “nagaram”
    输出: true

题解:
利用数组统计两个字符串中每个数字出现的次数,若频次相同,则说明他们包含的字符完全相同。

class Solution {
    public boolean isAnagram(String s, String t) {
    	//长度不相同直接返回 false
        if (s.length() != t.length()) {
            return false;
        }
        //字符串只包含26个字母可以初始化一个只有26个元素的统计频次的数组
        int[] counts = new int[26];
        //遍历字符串 s
        for (int i = 0; i < s.length(); ++i) {
        	//记录字符串 s 中字符出现的频次
            ++counts[s.charAt(i) - 'a'];
            //减去字符串 t 中出现该字符的频次
            --counts[t.charAt(i) - 'a'];
        }
        for (int i = 0; i < 26; ++i) {
        	//还有多余的字母返回 false
            if (counts[i] != 0) {
                return false;
            }
        }
        return true;
    }
}

执行用时: 5 ms 内存消耗: 38.7 MB
时间复杂度:O(n),n 为字符串 s 的长度
空间复杂度:O(S),S 为字符集大小,此处为 26

技巧:
字符串-‘a’的原因,根据ASCII码,26个字母减去小写字母 a 恰好对应数字 0~25

官方题解:

class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) {
            return false;
        }
        int[] table = new int[26];
        for (int i = 0; i < s.length(); i++) {
            table[s.charAt(i) - 'a']++;
        }
        for (int i = 0; i < t.length(); i++) {
            table[t.charAt(i) - 'a']--;
            if (table[t.charAt(i) - 'a'] < 0) {
                return false;
            }
        }
        return true;
    }
}

执行用时: 4 ms 内存消耗: 38.5 MB

  1. 同构字符串
    给定两个字符串 s 和 t,判断它们是否是同构的。
    如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。
    每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
    示例 1:
    输入:s = “egg”, t = “add”
    输出:true

题解:
将该问题转化为**记录两个字符串每个位置的第一次出现的位置,如果两个字符串中相同位置的字符与它们第一次出现的位置一样那么两个字符串同构。 **

class Solution {
    public boolean isIsomorphic(String s, String t) {
    	//在ASCII码范围创建数组,初始化为256
        int[] s_first_index = new int[256];
        int[] t_first_index = new int[256];
        for (int i = 0; i < s.length(); ++i) {
            if (s_first_index[s.charAt(i)] != t_first_index[t.charAt(i)]) {
                return false;
            }
            s_first_index[s.charAt(i)] = t_first_index[t.charAt(i)] = i + 1;
        }
        return true;
    }
}

执行用时: 7 ms 内存消耗: 38.1 MB
时间复杂度:O(n)
空间复杂度:O(S)

技巧:s_first_index[s.charAt(i)]
charAt()输出的字符串可按照ASCII码强制转换成 int 类型的数组

官方题解:

class Solution {
    public boolean isIsomorphic(String s, String t) {
        Map<Character, Character> s2t = new HashMap<Character, Character>();
        Map<Character, Character> t2s = new HashMap<Character, Character>();
        int len = s.length();
        for (int i = 0; i < len; ++i) {
            char x = s.charAt(i), y = t.charAt(i);
            if ((s2t.containsKey(x) && s2t.get(x) != y) || (t2s.containsKey(y) && t2s.get(y) != x)) {
                return false;
            }
            s2t.put(x, y);
            t2s.put(y, x);
        }
        return true;
    }
}

执行用时: 29 ms 内存消耗: 38.1 MB

  1. 回文子串
    给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
    回文字符串 是正着读和倒过来读一样的字符串。
    子字符串 是字符串中的由连续字符组成的一个序列。
    具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
    示例 1:
    输入:s = “abc”
    输出:3
    解释:三个回文子串: “a”, “b”, “c”
    示例 2:
    输入:s = “aaa”
    输出:6
    解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”

题解:
从字符串的 i 位置开始,向左和向右延长,判断当前位置 i 为中心的回文字符串存在多少,并返回 count

class Solution {
    public int countSubstrings(String s) {
        int count = 0;
        for (int i = 0; i < s.length(); ++i) {
        	//一个字符开始延长,奇数长度
            count += extendSubstrings(s, i, i);
            //两个字符开始延长,偶数长度
            count += extendSubstrings(s, i, i + 1);
        }
        return count;
    }
	//延长子字符串方法
    public int extendSubstrings(String s, int l, int r) {
        int count = 0;
        while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
            --l;//向左延长
            ++r;//向右延长
            ++count;//回文子字符串数目增加
        }
        return count;//返回当前位置的回文子字符串数目
    }
}

执行用时: 3 ms 内存消耗: 36.5 M
时间复杂度:O( n 2 n^2 n2),n为字符串长度,for 遍历一次,拓展字符串 while 遍历一次;
空间复杂度:O(1),不适用额外空间。

官方题解:
Manacher算法:在线性时间内求解最长回文子串的算法;
马拉车算法
原理:在所有的相邻字符中间插入 # ,使得无论奇数还是偶数都变成奇数,如 abaa 会被处理成 #a#b#a#a#

class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        //StringBuffer类可多次被修改且不产生新未使用对象
        StringBuffer t = new StringBuffer("$#");
        //遍历字符串 s 并形成新字符串 t

        for (int i = 0; i < n; ++i) {
            t.append(s.charAt(i));
            t.append('#');
        }
        n = t.length();
        t.append('!');

        int[] f = new int[n];
        int iMax = 0, rMax = 0, ans = 0;
        for (int i = 1; i < n; ++i) {
            // 初始化 f[i]
            f[i] = i <= rMax ? Math.min(rMax - i + 1, f[2 * iMax - i]) : 1;
            // 中心拓展
            while (t.charAt(i + f[i]) == t.charAt(i - f[i])) {
                ++f[i];
            }
            // 动态维护 iMax 和 rMax
            if (i + f[i] - 1 > rMax) {
                iMax = i;
                rMax = i + f[i] - 1;
            }
            // 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整
            ans += f[i] / 2;
        }

        return ans;
    }
}

执行用时: 2 ms 内存消耗: 36.4 M
时间复杂度:O(n)
空间复杂度:O(n)

  1. 计数二进制子串
    给定一个字符串 s,计算具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是连续的。
    重复出现的子串要计算它们出现的次数。
    示例 1 :
    输入: “00110011”
    输出: 6
    解释: 有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。
    请注意,一些重复出现的子串要计算它们出现的次数。
    另外,“00110011”不是有效的子串,因为所有的0(和1)没有组合在一起。
    示例 2 :
    输入: “10101”
    输出: 4
    解释: 有4个子串:“10”,“01”,“10”,“01”,它们具有相同数量的连续1和0。

题解:

class Solution {
    public int countBinarySubstrings(String s) {
        int pre = 0, cur = 1, count = 0;
        for (int i = 1; i < s.length(); ++i) {
            if (s.charAt(i) == s.charAt(i-1)) {
                ++cur;
            } else {
                pre = cur;
                cur = 1;
            }
            if (pre >= cur) {
                ++count;
            }
        }
        return count;
    }
}

执行用时: 10 ms 内存消耗: 38.8 M

官方题解:

class Solution {
    public int countBinarySubstrings(String s) {
        int ptr = 0, n = s.length(), last = 0, ans = 0;
        while (ptr < n) {
            char c = s.charAt(ptr);
            int count = 0;
            while (ptr < n && s.charAt(ptr) == c) {
                ++ptr;
                ++count;
            }
            ans += Math.min(count, last);
            last = count;
        }
        return ans;
    }
}

执行用时: 8 ms 内存消耗: 39.1 M
时间复杂度:O(n)
空间复杂度:O(1)

12.3 深入字符串

  1. 基本计算器 II
    给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
    整数除法仅保留整数部分。
    示例 1:
    输入:s = “3+2*2”
    输出:7

题解:

class Solution {
    public int calculate(String s) {
        int i = 0;
        return parseExpr(s, i);
    }

    public int parseExpr(String s, int i) {
        char op = '+';
        long left = 0, right = 0;
        while (i < s.length()) {
            if (s.charAt(i) != ' ') {
                long n = parseNum(s, i);
                switch (op) {
                    case '+' : left += right; right = n; break;
                    case '-' : left += right; right = -n; break;
                    case '*' : right *= n; break;
                    case '/' : right /= n; break;
                }
                if (i < s.length()) {
                    op = s.charAt(i);
                }
            }
            ++i;
        }
        return (int) (left + right);
    }

    public long parseNum(String s, int i) {
        long n = 0;
        while (i < s.length() && Character.isDigit(s.charAt(i))) {
            n = 10 * n + (s.charAt(i++) - '0');
        }
        return n;
    }
}

执行用时: 10 ms 内存消耗: 38.5 M

官方题解:

class Solution {
    public int calculate(String s) {
        Deque<Integer> stack = new LinkedList<Integer>();
        char preSign = '+';
        int num = 0;
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            if (Character.isDigit(s.charAt(i))) {
                num = num * 10 + s.charAt(i) - '0';
            }
            if (!Character.isDigit(s.charAt(i)) && s.charAt(i) != ' ' || i == n - 1) {
                switch (preSign) {
                    case '+':
                        stack.push(num);
                        break;
                    case '-':
                        stack.push(-num);
                        break;
                    case '*':
                        stack.push(stack.pop() * num);
                        break;
                    default:
                        stack.push(stack.pop() / num);
                }
                preSign = s.charAt(i);
                num = 0;
            }
        }
        int ans = 0;
        while (!stack.isEmpty()) {
            ans += stack.pop();
        }
        return ans;
    }
}

执行用时: 13 ms 内存消耗: 41.6 M

12.4 字符串匹配

  1. 实现 strStr()
    实现 strStr() 函数。
    给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
    说明:
    当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
    对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。
    示例 1:
    输入:haystack = “hello”, needle = “ll”
    输出:2

题解:
Knuth-Morris-Patt,KMP算法

class Solution {
    public int strStr(String haystack, String needle) {
        int n = haystack.length(), m = needle.length();
        if (m == 0) {
            return 0;
        }
        int[] pi = new int[m];
        for (int i = 1, j = 0; i < m; i++) {
            while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
                j = pi[j - 1];
            }
            if (needle.charAt(i) == needle.charAt(j)) {
                j++;
            }
            pi[i] = j;
        }
        for (int i = 0, j = 0; i < n; i++) {
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                j = pi[j - 1];
            }
            if (haystack.charAt(i) == needle.charAt(j)) {
                j++;
            }
            if (j == m) {
                return i - m + 1;
            }
        }
        return -1;
    }
}

执行用时: 5 ms 内存消耗: 38.3 M
时间复杂度:O(m+n),m 是字符串 needle 的长度,n 是字符串 haystack 的长度,至多需要分别遍历两个字符串一次;
空间复杂度:O(m),m 是字符串 needle 的长度,只需要保存字符串 needle 的前缀函数。

13 链表

13.1 数据结构概念

链表:由数据和指针构成,链表的指针指向下一个节点。

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; 
    }
}

Java链表语法操作

13.2 链表的基本操作

  1. 反转链表
    给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
    示例 1:
    在这里插入图片描述
    输入:head = [1,2,3,4,5]
    输出:[5,4,3,2,1]

题解:
递归解法

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode newHead = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }
}

执行用时: 0 ms 内存消耗: 38.4 M

题解:
非递归解法

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
}

执行用时: 0 ms 内存消耗: 38.2 M

  1. 合并两个有序链表
    将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
    示例 1:
    在这里插入图片描述
    输入:l1 = [1,2,4], l2 = [1,3,4]
    输出:[1,1,2,3,4,4]

题解:
非递归

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode prehead = new ListNode(-1);

        ListNode prev = prehead;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
               prev.next = l1;
               l1 = l1.next; 
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
            prev = prev.next;
        }
        prev.next = l1 == null ? l2 : l1;
        return prehead.next;
    }
}

执行用时: 0 ms 内存消耗: 38.2 M

题解:
递归

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l2 == null) {
            return l1;
        }
        if (l1 == null) {
            return l2;
        }
        if (l1.val > l2.val) {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    }
}

执行用时: 0 ms 内存消耗: 37.9 M

  1. 两两交换链表中的节点
    给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
    你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
    示例 1:
    在这里插入图片描述
    输入:head = [1,2,3,4]
    输出:[2,1,4,3]

题解:
非递归

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode pre = new ListNode(0);
        pre.next = head;
        ListNode temp = pre;
        while(temp.next != null && temp.next.next != null) {
            ListNode start = temp.next;
            ListNode end = temp.next.next;
            temp.next = end;
            start.next = end.next;
            end.next = start;
            temp = start;
        }
        return pre.next;
    }
}

执行用时: 0 ms 内存消耗: 36 M

题解:
递归

class Solution {
    public ListNode swapPairs(ListNode head) {
        if(head == null || head.next == null){
            return head;
        }
        ListNode next = head.next;
        head.next = swapPairs(next.next);
        next.next = head;
        return next;
    }
}

执行用时: 0 ms 内存消耗: 36.1 M

13.3 链表的晋级操作

  1. 相交链表
    给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
    图示两个链表在节点 c1 开始相交:
    在这里插入图片描述
    题目数据 保证 整个链式结构中不存在环。
    注意,函数返回结果后,链表必须 保持其原始结构 。
    示例 1:
    在这里插入图片描述
    输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
    输出:Intersected at ‘8’
    解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
    从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
    在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

题解:

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        ListNode l1 = headA, l2 = headB;
        while (l1 != l2) {
            l1 = l1 != null ? l1.next : headB;
            l2 = l2 != null ? l2.next : headA;
        }
        return l1;
    }
}

执行用时: 1 ms 内存消耗: 41.3 M

  1. 回文链表
    给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
    示例 1:
    在这里插入图片描述
    输入:head = [1,2,2,1]
    输出:true

题解:

class Solution {
        public boolean isPalindrome(ListNode head) {
        if(head == null || head.next == null) {
            return true;
        }
        ListNode slow = head, fast = head;
        ListNode pre = head, prepre = null;
        while(fast != null && fast.next != null) {
            pre = slow;
            slow = slow.next;
            fast = fast.next.next;
            pre.next = prepre;
            prepre = pre;
        }
        if(fast != null) {
            slow = slow.next;
        }
        while(pre != null && slow != null) {
            if(pre.val != slow.val) {
                return false;
            }
            pre = pre.next;
            slow = slow.next;
        }
        return true;
    }
}

执行用时: 3 ms 内存消耗: 48 M

14 树

14.1 数据结构概念

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;
	}
}

14.2 树的递归

  1. 二叉树的最大深度
    给定一个二叉树,找出其最大深度。
    二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
    说明: 叶子节点是指没有子节点的节点。
    示例:
    给定二叉树 [3,9,20,null,null,15,7],
    在这里插入图片描述
    返回它的最大深度 3 。

题解:

class Solution {
    public int maxDepth(TreeNode root) {
        return root != null ? 1 + Math.max(maxDepth(root.left), maxDepth(root.right)) : 0;
    }
}

执行用时: 3 ms 内存消耗: 48 M

  1. 平衡二叉树
    给定一个二叉树,判断它是否是高度平衡的二叉树。
    本题中,一棵高度平衡二叉树定义为:
    一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
    示例 1:
    在这里插入图片描述
    输入:root = [3,9,20,null,null,15,7]
    输出:true

题解:

class Solution {
    public boolean isBalanced(TreeNode root) {
        return helper(root) != -1;
    }

    public int helper(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = helper(root.left), right = helper(root.right);
        if (left == -1 || right == -1 || Math.abs(left - right) > 1) {
            return -1;
        }
        return 1 + Math.max(left, right);
    }
}

执行用时: 1 ms 内存消耗: 38.2 M

  1. 二叉树的直径
    给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
    示例 :
    给定二叉树
    在这里插入图片描述
    返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
    注意:
    两结点之间的路径长度是以它们之间边的数目表示。

题解:

class Solution {
    int diameter;
    
    public int diameterOfBinaryTree(TreeNode root) {
        diameter = 0;
        helper(root);
        return diameter;
    }

    public int helper(TreeNode node) {
        if (node == null) return 0;
        int left = helper(node.left), right = helper(node.right);
        diameter = Math.max(diameter, left + right);
        return Math.max(left, right) + 1;
    }
}

执行用时: 0 ms 内存消耗: 37.9 M

  1. 路径总和 III
    给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
    路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
    示例 1:
    在这里插入图片描述
    输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
    输出:3
    解释:和等于 8 的路径有 3 条,如图所示。

题解:

class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        return root != null ? pathSumStartWithRoot(root, targetSum) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum) : 0;
    }

    public int pathSumStartWithRoot(TreeNode root, int targetSum) {
        if (root == null) return 0;
        int count = root.val == targetSum ? 1 : 0;
        count += pathSumStartWithRoot(root.left, targetSum - root.val);
        count += pathSumStartWithRoot(root.right, targetSum - root.val);
        return count;
    }
}

执行用时: 26 ms 内存消耗: 38.4 M

  1. 对称二叉树
    给定一个二叉树,检查它是否是镜像对称的。
    例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
    在这里插入图片描述
    但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
    在这里插入图片描述
    进阶:
    你可以运用递归和迭代两种方法解决这个问题吗?

题解:
递归

class Solution {
    public boolean isSymmetric(TreeNode root) {
        return root != null ? isSymmetric(root.left, root.right) : true;
    }

    public boolean isSymmetric(TreeNode left, TreeNode right) {
        if (left == null && right == null) return true;
        if (left == null || right == null) return false;
        if (left.val != right.val) return false;
        return isSymmetric(left.left, right.right) && isSymmetric(left.right, right.left);
    }
}

执行用时: 0 ms 内存消耗: 36.3 M

官方题解:
迭代

class Solution {
    public boolean isSymmetric(TreeNode root) {
        return check(root, root);
    }

    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;
    }
}

执行用时: 1 ms 内存消耗: 37.8 M

  1. 删点成林
    给出二叉树的根节点 root,树上每个节点都有一个不同的值。
    如果节点值在 to_delete 中出现,我们就把该节点从树上删去,最后得到一个森林(一些不相交的树构成的集合)。
    返回森林中的每棵树。你可以按任意顺序组织答案。
    示例:
    在这里插入图片描述
    输入:root = [1,2,3,4,5,6,7], to_delete = [3,5]
    输出:[[1,2,null,4],[6],[7]]

题解:

class Solution {
    //map存储要删的节点的值
    private boolean[] map = new boolean[1001];
    //ans为要输出的结果
    private List<TreeNode> ans = new ArrayList<>();
    public List<TreeNode> delNodes(TreeNode root, int[] to_delete) {
        int n = to_delete.length;
        for (int i = 0; i < n; i++) {
            map[to_delete[i]] = true;
        }
        //先看看根结点要不要删,不删的话就加进去
        if (!map[root.val]) {
            ans.add(root);
            search(root, true);
        } else {
            search(root, false);
        }
        return ans;
    }
    //子方法输入参数的布尔值为当前节点要不要删,即是否还存在?
    //当前节点是否存在决定着接下来的左右节点是否要加入结果集
    public void search(TreeNode root, boolean exist) {
        if (root == null) return;
        //如果当前节点不存在了,且左节点不要删,就把左节点加入集合
        //右节点同理
        if (!exist) {
            if (root.left != null && !map[root.left.val]) {
                ans.add(root.left);
            }
            if (root.right != null && !map[root.right.val]) {
                ans.add(root.right);
            }
        }
        //不管当前节点还是否存在,如果左节点要删则在深入下一层时传false,表示这个节点已经不存在了
        //这里的不存在并不代表已经删除,因为删除操作即断开二叉链表的操作是在递归完这个节点的后续层之后的
        //这里的不存在相当于形存神灭的状态,物理上还存在,但精神上已经死亡即早晚要删先留你个躯壳
        //右节点同理
        if (root.left != null && map[root.left.val]) {
            search(root.left, false);
            root.left = null;
        } else {
            search(root.left, true);
        }
        if (root.right != null && map[root.right.val]) {
            search(root.right, false);
            root.right = null;
        } else {
            search(root.right, true);
        }
    }
}

执行用时: 1 ms 内存消耗: 38.7 M

14.3 层序遍历

概念:使用 BFS 进行层序遍历;
不需要使用两个队列储存当前层和下一层的节点:当前队列中的节点数 = 当前层的节点数。

  1. 二叉树的层平均值
    给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。
    示例 1:
    输入:
    在这里插入图片描述
    输出:[3, 14.5, 11]
    解释:
    第 0 层的平均值是 3 , 第1层是 14.5 , 第2层是 11 。因此返回 [3, 14.5, 11] 。

题解:
广度优先搜索:从根节点开始搜索,每一轮遍历同一层的全部节点,计算该层的节点数以及该层的节点值之和,然后计算该层的平均值。

在这里插入图片描述

class Solution {
    public List<Double> averageOfLevels(TreeNode root) {
    	//新建返回值链表
        List<Double> ans = new LinkedList<>();
        //如果根节点为空直接返回
        if (root == null) return ans;
        //创建队列保存当前层的节点
        Queue<TreeNode> q = new LinkedList<>();
        //初始加入根节点
        q.offer(root);
        while (!q.isEmpty()) {
            int count = q.size();
            double sum = 0;
            for (int i = 0; i < count; ++i) {
                TreeNode node = q.poll();
                sum += node.val;

                if (node.left != null) q.offer(node.left);
                if (node.right != null) q.offer(node.right);
            }
            ans.add(sum / count);
        }
        return ans;
    }
}

执行用时: 2 ms 内存消耗: 40.3 M
时间复杂度:O(n)
空间复杂度:O(n)

官方题解:
深度优先搜索

class Solution {
    public List<Double> averageOfLevels(TreeNode root) {
        List<Integer> counts = new ArrayList<Integer>();
        List<Double> sums = new ArrayList<Double>();
        dfs(root, 0, counts, sums);
        List<Double> averages = new ArrayList<Double>();
        int size = sums.size();
        for (int i = 0; i < size; i++) {
            averages.add(sums.get(i) / counts.get(i));
        }
        return averages;
    }

    public void dfs(TreeNode root, int level, List<Integer> counts, List<Double> sums) {
        if (root == null) {
            return;
        }
        if (level < sums.size()) {
            sums.set(level, sums.get(level) + root.val);
            counts.set(level, counts.get(level) + 1);
        } else {
            sums.add(1.0 * root.val);
            counts.add(1);
        }
        dfs(root.left, level + 1, counts, sums);
        dfs(root.right, level + 1, counts, sums);
    }
}

执行用时: 1 ms 内存消耗: 40.4 M
时间复杂度:O(n)
空间复杂度:O(n)

14.4 前中后序遍历

深度优先搜索的三种遍历方式:
在这里插入图片描述
①前序遍历:先遍历父节点,再遍历左节点,最后遍历右节点,遍历顺序为

1→2→4→5→3→6

void preorder(TreeNode root) {
	visit(root);
	preorder(root.left);
	preorder(root.right);
}

②中序遍历:先遍历左节点,再遍历父节点,最后遍历左节点,遍历顺序为

4→2→5→1→3→6

void inorder(TreeNode root) {
	inorder(root.left);
	visit(root);
	inorder(root.right);
}

③后序遍历:先遍历左节点,再遍历右节点,最后遍历父节点,遍历顺序为

4→5→2→6→3→1

void postorder(TreeNode root) {
	postorder(root.left);
	postorder(root.right);
	visit(root);
}
	

参考下面文章:
一文学会二叉树前中后序遍历

  1. 从前序与中序遍历序列构造二叉树
    给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。
    示例 1:
    在这里插入图片描述
    Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
    Output: [3,9,20,null,null,15,7]

题解:
哈希表把中序遍历数组的每个元素的值和下标存起来,这样寻找根节点的位置就可以直接得到了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
    	//哈希表储存中序遍历数组的每个元素的值和下标
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) {
            map.put(inorder[i], i);
        }
        //返回辅助函数的结果
        return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length, map);
    }
	//辅助函数
    private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end, HashMap<Integer, Integer> map) {
    	//开始结束位置相等,preorder为空,直接返回null
        if (p_start == p_end) {
            return null;
        }
        int root_val = preorder[p_start];
        TreeNode root = new TreeNode(root_val);
        int i_root_index = map.get(root_val);
        int leftNum = i_root_index - i_start;
        //遍历构建左子树
        root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index, map);
        //遍历构建右子树
        root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end, map);
        //返回根节点
        return root;
    }
}

执行用时: 1 ms 内存消耗: 38.3 M
时间复杂度:O(n),n 为树的节点数;
空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(n)O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h<n,所以总空间复杂度为 O(n)。

  1. 二叉树的前序遍历
    给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
    示例 1:
    在这里插入图片描述
    输入:root = [1,null,2,3]
    输出:[1,2,3]

题解:
迭代
递归隐式地维护一个栈,迭代显示地维护一个栈。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
    	//新建返回链表
        List<Integer> res = new ArrayList<Integer>();
        if (root == null) {
            return res;
        }
        //创建队列储存链表节点
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        //初始化 node 为根节点
        TreeNode node = root;
        //判断终止条件
        while (!stack.isEmpty() || node != null) {
            while (node != null) {
            	//根节点值加入返回结果
                res.add(node.val);
                stack.push(node);
                //遍历左子节点
                node = node.left;
            }
            node = stack.pop();
            node = node.right;
        }
        return res;
    }
}

执行用时: 0 ms 内存消耗: 36.7 M
时间复杂度:O(n), n 为二叉树节点数,每个节点被遍历一次;
空间复杂度:O(n),空间为递归中栈的开销,平均为O(logn),最坏情况为O(n)

题解:
递归

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        preorder(root, res);
        return res;
    }

    public void preorder(TreeNode root, List<Integer> res) {
    	//递归终止条件,根节点为空
        if (root == null) {
            return;
        }
        //当前根节点值加入返回结果中
        res.add(root.val);
        //遍历左子树
        preorder(root.left, res);
        //遍历右子树
        preorder(root.right, res);
    }
}

执行用时: 0 ms 内存消耗: 36.8 M
时间复杂度:O(n), n 为二叉树节点数,每个节点被遍历一次;
空间复杂度:O(n),空间为递归中栈的开销,平均为O(logn),最坏情况为O(n)

题解:
Morris遍历
Morris遍历可以将非递归遍历中的空间复杂度降为O(1)。从而实现时间复杂度为O(N),而空间复杂度为O(1)的精妙算法。
实质
建立一种机制,对于没有左子树的节点只到达一次,对于有左子树的节点会到达两次。
实现
1、记作当前节点为cur。
2、如果 cur 无左子树,cur 向右移动(cur=cur.right)
3、如果 cur 有左子树,找到 cur 左子树上最右的节点,记为 mostright
3.1、如果 mostright 的 right 指针指向空,让其指向 cur,cur 向左移动(cur=cur.left)
3.2、如果 mostright 的 right 指针指向 cur,让其指向空,cur 向右移动(cur=cur.right)

新加的指向关系,就是找到根节点左子树的最右子树,然后将最右子树的right指向根节点。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        if (root == null) {
            return res;
        }

        TreeNode p1 = root, p2 = null;

        while (p1 != null) {
            p2 = p1.left;
            if (p2 != null) {
                while (p2.right != null && p2.right != p1) {
                    p2 = p2.right;
                }
                if (p2.right == null) {
                    res.add(p1.val);
                    p2.right = p1;
                    p1 = p1.left;
                    //进入下一次迭代
                    continue;
                } else {
                    p2.right = null;
                }
            } else {
                res.add(p1.val);
            }
            p1 = p1.right;
        }
        return res;
    }
}

执行用时: 0 ms 内存消耗: 36.4 M
时间复杂度:O(n)
空间复杂度:O(1)

continue 与 break 语句的区别在于:continue 并不是中断循环语句,而是中止当前迭代的循环,进入下一次的迭代。

14.5 二叉查找树

概念:二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。

  1. 恢复二叉搜索树
    给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
    进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?
    示例 1:
    在这里插入图片描述
    输入:root = [1,3,null,null,2]
    输出:[3,1,null,null,2]
    解释:3 不能是 1 左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。

题解:
深度优先搜索+递归

class Solution {
    public void recoverTree(TreeNode root) {
        List<TreeNode> list = new ArrayList<TreeNode>();
        dfs(root,list);
        TreeNode x = null;
        TreeNode y = null;
        //扫面遍历的结果,找出可能存在错误交换的节点x和y
        for(int i=0;i<list.size()-1;++i) {
            if(list.get(i).val>list.get(i+1).val) {
                y = list.get(i+1);
                if(x==null) {
                    x = list.get(i);
                }
            }
        }
        //如果x和y不为空,则交换这两个节点值,恢复二叉搜索树
        if(x!=null && y!=null) {
            int tmp = x.val;
            x.val = y.val;
            y.val = tmp;
        }
    }

    //中序遍历二叉树,并将遍历的结果保存到list中        
    private void dfs(TreeNode node,List<TreeNode> list) {
        if(node==null) {
            return;
        }
        dfs(node.left,list);
        list.add(node);
        dfs(node.right,list);
    }
}

执行用时: 2 ms 内存消耗: 38.8 M
时间复杂度:O(n)
空间复杂度:O(n)

题解:
Morris算法
在这里插入图片描述

新加的指向关系,就是找到根节点左子树的最右子树,然后将最右子树的right指向根节点。

class Solution {
    public void recoverTree(TreeNode root) {
    	//记录错误的两个值
    	TreeNode x = null, y = null;
    	//记录中序遍历当前节点的前驱
    	TreeNode pre = null;
    	//用来完成Morris连接的寻找前驱的指针
    	TreeNode predecessor = null;
    	while(root != null) {
    		if(root.left != null) {//左子树不为空,1、链接root节点的前驱,他的前驱还没访问,所以root不能现在访问,继续访问root左子树  2、root节点访问,并且断开root节点的前驱连接,然后访问root的右子树
    			predecessor = root.left;
    			while(predecessor.right != null && predecessor.right != root) {
    				predecessor = predecessor.right;
    			}
    			if(predecessor.right == root) {//说明了1已经执行过了,执行2
    				//访问
    				if(pre != null && pre.val > root.val) {
    					if(x == null) x = pre;
    					y = root;
    				}
    				//更新前驱,为下一个节点做准备
    				pre = root;
    				//断开前驱连接
    				predecessor.right = null;
    				//访问root右子树
    				root = root.right;
    			}else {//predecessor.right != root,说明了还没执行过1
    				predecessor.right = root;
    				root = root.left;
    			}
    		}else {//root.left == null,root不需要链接节点的前驱(他的前驱其实就是pre(第一个节点pre为null),且已经被访问过了),那么直接访问root
				//访问
				if(pre != null && pre.val > root.val) {
					if(x == null) x = pre;
					y = root;
				}
				//更新前驱,为下一个节点做准备
				pre = root;
				//访问root的右子树
				root = root.right;
    		}
    	}
    	swap(x, y);
    }
    
    public void swap(TreeNode x, TreeNode y) {
        int tmp = x.val;
        x.val = y.val;
        y.val = tmp;
    }
}

执行用时: 2 ms 内存消耗: 38.7 M
时间复杂度:O(n)
空间复杂度:O(1)

  1. 修剪二叉搜索树
    给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树不应该改变保留在树中的元素的相对结构(即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在唯一的答案。
    所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。
    示例 1:
    在这里插入图片描述
    输入:root = [1,0,2], low = 1, high = 2
    输出:[1,null,2]

题解:

/**
 * 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 trimBST(TreeNode root, int low, int high) {
        if (root == null) return root;
        if (root.val > high) return trimBST(root.left, low, high);
        if (root.val < low) return trimBST(root.right, low, high);
        root.left = trimBST(root.left, low, high);
        root.right = trimBST(root.right, low, high);
        return root;
    }
}

执行用时: 0 ms 内存消耗: 38 M

14.6 字典树

  1. 实现 Trie (前缀树)
    Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
    请你实现 Trie 类:
    Trie() 初始化前缀树对象。
    void insert(String word) 向前缀树中插入字符串 word 。
    boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
    boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
    示例:
    输入
    [“Trie”, “insert”, “search”, “search”, “startsWith”, “insert”, “search”]
    [[], [“apple”], [“apple”], [“app”], [“app”], [“app”], [“app”]]
    输出
    [null, null, true, false, true, null, true]
    解释
    Trie trie = new Trie();
    trie.insert(“apple”);
    trie.search(“apple”); // 返回 True
    trie.search(“app”); // 返回 False
    trie.startsWith(“app”); // 返回 True
    trie.insert(“app”);
    trie.search(“app”); // 返回 True

题解:

class Trie {
    class TrieNode {
        boolean end;
        TrieNode[] tns = new TrieNode[26];
    }

    TrieNode root;
    public Trie() {
        root = new TrieNode();
    }

    public void insert(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new TrieNode();
            p = p.tns[u]; 
        }
        p.end = true;
    }

    public boolean search(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) return false;
            p = p.tns[u]; 
        }
        return p.end;
    }

    public boolean startsWith(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) return false;
            p = p.tns[u]; 
        }
        return true;
    }
}

执行用时: 34 ms 内存消耗: 47.4 M

15 图

15.1 数据结构概念

图基本概念:图(Graph)是一种复杂的非线性结构,在图结构中,每个元素都可以有零个或多个前驱,也可以有零个或多个后继,也就是说,元素之间的关系是任意的。
图的表示方法
有 n 个节点, m 条边的图,
①邻接矩阵(adjacency matrix)表示法:

15.2 二分图

二分图又称作二部图,是图论中的一种特殊模型。 设 G=(V,E) 是一个无向图,如果顶点 V 可分割为两个互不相交的子集 (A,B),并且图中的每条边(i,j)所关联的两个顶点 i 和 j 分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
二分图算法也成为染色法,是广度优先搜索的一种。如果可以用两种颜色对图中的节点进行着色,并且相邻节点颜色不同,则图为二分图。
在这里插入图片描述

  1. 判断二分图
    存在一个 无向图 ,图中有 n 个节点。其中每个节点都有一个介于 0 到 n - 1 之间的唯一编号。给你一个二维数组 graph ,其中 graph[u] 是一个节点数组,由节点 u 的邻接节点组成。形式上,对于 graph[u] 中的每个 v ,都存在一条位于节点 u 和节点 v 之间的无向边。该无向图同时具有以下属性:
    不存在自环(graph[u] 不包含 u)。
    不存在平行边(graph[u] 不包含重复值)。
    如果 v 在 graph[u] 内,那么 u 也应该在 graph[v] 内(该图是无向图)
    这个图可能不是连通图,也就是说两个节点 u 和 v 之间可能不存在一条连通彼此的路径。
    二分图 定义:如果能将一个图的节点集合分割成两个独立的子集 A 和 B ,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为 二分图 。
    如果图是二分图,返回 true ;否则,返回 false 。
    示例 1:
    在这里插入图片描述
    输入:graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
    输出:false
    解释:不能将节点分割成两个独立的子集,以使每条边都连通一个子集中的一个节点与另一个子集中的一个节点。

题解:

class Solution {
    public boolean isBipartite(int[][] graph) {
        int n = graph.length;
        int[] color = new int[n];
        Queue<Integer> q = new LinkedList<>();
        for (int i = 0; i < n; ++i) {
            if (color[i] != 0) {
                continue;
            }
            q.offer(i);
            color[i] = 1;
            while (!q.isEmpty()) {
                int node = q.poll();
                for (int j : graph[node]) {
                    if (color[j] == 0) {
                        q.offer(j);
                        color[j] = -color[node];
                    } else if (color[node] == color[j]) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
}

执行用时: 1 ms 内存消耗: 39.1 M

15.3 拓扑排序

  1. 课程表 II
    现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。
    例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
    返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
    示例 1:
    输入:numCourses = 2, prerequisites = [[1,0]]
    输出:[0,1]
    解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

题解:

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        if (numCourses <= 0) {
            return new int[0];
        }

        HashSet<Integer>[] adj = new HashSet[numCourses];
        for (int i = 0; i < numCourses; i++) {
            adj[i] = new HashSet<>();
        }

        // [1,0] 0 -> 1
        int[] inDegree = new int[numCourses];
        for (int[] p : prerequisites) {
            adj[p[1]].add(p[0]);
            inDegree[p[0]]++;
        }

        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        int[] res = new int[numCourses];
        // 当前结果集列表里的元素个数,正好可以作为下标
        int count = 0;

        while (!queue.isEmpty()) {
            // 当前入度为 0 的结点
            Integer head = queue.poll();
            res[count] = head;
            count++;

            Set<Integer> successors = adj[head];
            for (Integer nextCourse : successors) {
                inDegree[nextCourse]--;
                // 马上检测该结点的入度是否为 0,如果为 0,马上加入队列
                if (inDegree[nextCourse] == 0) {
                    queue.offer(nextCourse);
                }
            }
        }
        
        // 如果结果集中的数量不等于结点的数量,就不能完成课程任务,这一点是拓扑排序的结论
        if (count == numCourses) {
            return res;
        }
        return new int[0];
    }
}

执行用时: 7 ms 内存消耗: 39.6 M

16 复杂数据结构

16.1 并查集

  1. 冗余连接
    树可以看成是一个连通且 无环 的 无向 图。
    给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。
    请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。
    示例 1:
    在这里插入图片描述
    输入: edges = [[1,2], [1,3], [2,3]]
    输出: [2,3]

题解:

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        int nodesCount = edges.length;
        int[] parent = new int[nodesCount + 1];
        for (int i = 1; i <= nodesCount; i++) {
            parent[i] = i;
        }
        for (int i = 0; i < nodesCount; i++) {
            int[] edge = edges[i];
            int node1 = edge[0], node2 = edge[1];
            if (find(parent, node1) != find(parent, node2)) {
                union(parent, node1, node2);
            } else {
                return edge;
            }
        }
        return new int[0];
    }

    public void union(int[] parent, int index1, int index2) {
        parent[find(parent, index1)] = find(parent, index2);
    }

    public int find(int[] parent, int index) {
        if (parent[index] != index) {
            parent[index] = find(parent, parent[index]);
        }
        return parent[index];
    }
}

执行用时: 1 ms 内存消耗: 39 M

16.2 复合数据结构

  1. LRU 缓存机制
    运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
    实现 LRUCache 类:
    LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
    int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
    void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
    **进阶:**你是否可以在 O(1) 时间复杂度内完成这两种操作?
    示例:
    输入
    [“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
    [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
    输出
    [null, null, null, 1, null, -1, null, -1, 3, 4]
    解释
    LRUCache lRUCache = new LRUCache(2);
    lRUCache.put(1, 1); // 缓存是 {1=1}
    lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
    lRUCache.get(1); // 返回 1
    lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
    lRUCache.get(2); // 返回 -1 (未找到)
    lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
    lRUCache.get(1); // 返回 -1 (未找到)
    lRUCache.get(3); // 返回 3
    lRUCache.get(4); // 返回 4

题解:

class LRUCache extends LinkedHashMap<Integer, Integer>{
    private int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    // 这个可不写
    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity; 
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

执行用时: 42 ms 内存消耗: 107.2 M

这篇关于【算法】一文刷完LeetCode全部典型题(持续更新)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!