0003.无重复字符的最长子串.md

关键字 : 字符串 重复 最长子串 滑动窗口

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,”pwke” 是一个子序列,不是子串。

  1. public int lengthOfLongestSubstring(String s) {
  2. if (s.length() <= 1){
  3. return s.length();
  4. }
  5. // 子串起点
  6. int startIndex = 0;
  7. // 记录最长子串长度
  8. int maxLen = 1;
  9. // 窗口右边界不断右移
  10. for (int endIndex = 1; endIndex < s.length(); endIndex++) {
  11. int tempLen = 1;
  12. // 每次右边界右移一位就需要判断窗口中是否存在当前右边界字符
  13. // 如果窗口中存在右边界字符, 则窗口左移动到窗口中重复元素的后一个位置
  14. for (int i = startIndex; i < endIndex ; i++) {
  15. if (s.charAt(i) == s.charAt(endIndex)){
  16. startIndex = i+1;
  17. break;
  18. }
  19. }
  20. // 更新答案
  21. maxLen = Math.max(maxLen, endIndex-startIndex+1);
  22. }
  23. return maxLen;
  24. }

0005.最长回文子串.md

关键字 : 字符串 最长回文串 动归

给你一个字符串 s,找到 s 中最长的回文子串。

  1. public String longestPalindrome(String s) {
  2. // 特殊用例判断
  3. int len = s.length();
  4. if (len < 2) {
  5. return s;
  6. }
  7. int maxLen = 1;
  8. int start = 0;
  9. // dp[i][j] 表示 s[i, j] 是否是回文串
  10. boolean[][] dp = new boolean[len][len];
  11. char[] charArray = s.toCharArray();
  12. // 单个的字符肯定是回文串
  13. for (int i = 0; i < len; i++) {
  14. dp[i][i] = true;
  15. }
  16. // 计算dp[i][j]
  17. for (int j = 1; j < len; j++) {
  18. for (int i = 0; i < j; i++) {
  19. if (charArray[i] != charArray[j]) {
  20. dp[i][j] = false;
  21. } else {
  22. if (j - i < 3) {
  23. dp[i][j] = true;
  24. } else {
  25. dp[i][j] = dp[i + 1][j - 1];
  26. }
  27. }
  28. // 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置
  29. if (dp[i][j] && j - i + 1 > maxLen) {
  30. maxLen = j - i + 1;
  31. start = i;
  32. }
  33. }
  34. }
  35. return s.substring(start, start + maxLen);
  36. }

0007.K个一组反转链表.md

关键字 : 链表 反转

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

  1. // 使用一个固定长度的栈
  2. public static ListNode reverseKGroup(ListNode head, int k) {
  3. ListNode p = head;
  4. // 栈的大小为k
  5. ArrayDeque<ListNode> stack = new ArrayDeque<>(k);
  6. ListNode pt = new ListNode(-1);
  7. ListNode ans = pt;
  8. // p指针往后走的过程中, 将节点入栈
  9. while (p != null){
  10. stack.addFirst(p);
  11. p = p.next;
  12. // 栈中有K个节点了
  13. if (stack.size() == k){
  14. // 对栈中节点进行反转
  15. // pt指向的是当前反转的最后一个节点
  16. while (!stack.isEmpty()){
  17. pt.next = stack.removeFirst();
  18. pt = pt.next;
  19. }
  20. }
  21. }
  22. // 最后栈中还有元素, 处理剩下的不满k个的节点
  23. // 此处处理方式是, 保留原链表顺序, 即将stack以队列方式出队
  24. while (!stack.isEmpty()){
  25. pt.next = stack.removeLast();
  26. pt = pt.next;
  27. }
  28. pt.next = null;
  29. return ans.next;
  30. }

0009. 数组中的第K个最大元素.md

关键字 : 数组 第k大 快排 堆排序 优先队列

给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例:

  1. 输入: [3,2,1,5,6,4] k = 2
  2. 输出: 5

方法一

  1. // 使用优先队列(默认底层实现为最小堆)
  2. public int findKthLargest(int[] nums, int k) {
  3. PriorityQueue<Integer> pq = new PriorityQueue<>();
  4. for (int i = 0; i < k; i++) {
  5. pq.add(nums[i]);
  6. }
  7. for (int i = k; i < nums.length; i++) {
  8. if (!pq.isEmpty() && nums[i] > pq.peek()){
  9. pq.remove();
  10. pq.add(nums[i]);
  11. }
  12. }
  13. return pq.peek();
  14. }

方法二

  1. // 利用快排过程
  2. private int k;
  3. private int ans;
  4. public int findKthLargest1(int[] nums, int k) {
  5. this.k = k;
  6. sort(nums, 0, nums.length-1);
  7. return ans;
  8. }
  9. public void sort(int[] arr, int left, int right){
  10. if (left >= right) return;
  11. int p = partition(arr, left, right);
  12. // 第k大的对应第arr.length - k小的(从0开始算)
  13. if (p == arr.length - k){
  14. ans = arr[p];
  15. return;
  16. }
  17. // 左边排序
  18. sort(arr, left, p-1);
  19. // 右边排序
  20. sort(arr, p+1, right);
  21. }
  22. private int partition(int[] arr, int left, int right) {
  23. // 优化点 : 随机选取枢轴元素. 将选中的枢轴元素交换到最左边
  24. int p = left + (new Random()).nextInt(right-left+1);
  25. swap(arr, left, p);
  26. // 双指针移动
  27. int i = left+1, j = right;
  28. while (true){
  29. // 从左往右找到第一个比枢纽元素大的, 注意循环条件
  30. while (i <= j && arr[i] < arr[left])
  31. i++;
  32. // 从右往左找到第一个比枢纽元素小的,注意循环条件
  33. while (i <= j && arr[j] > arr[left])
  34. j--;
  35. // 循环终止条件
  36. if (i >= j) break;
  37. // 交换两个位置的元素
  38. swap(arr, i, j);
  39. // i往后走一步, j往前走一步, 继续循环
  40. i++;
  41. j--;
  42. }
  43. // 最后交换枢纽元素和j停留的位置
  44. swap(arr, left, j);
  45. return j;
  46. }
  47. private void swap(int[] arr, int i, int j){
  48. int temp = arr[i];
  49. arr[i] = arr[j];
  50. arr[j] = temp;
  51. }

0015.三数之和.md

关键字 : 数组 三数之和 排序 双指针

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复答案。

思路 : 排序+双指针

时间复杂度 O(n^2)

  1. public static List<List<Integer>> threeSum(int[] nums) {
  2. List<List<Integer>> ans = new ArrayList<>();
  3. // 先排序
  4. Arrays.sort(nums);
  5. for (int i = 0; i < nums.length - 1; i++) {
  6. // 如果首元素大于0, 则后面不可能找出两个数的和=-首元素
  7. if(nums[i] > 0) return ans;
  8. // 跳过重复解
  9. if (i>0 && nums[i] == nums[i-1]){
  10. continue;
  11. }
  12. int l = i+1;
  13. int r = nums.length-1;
  14. while (l<r){
  15. if (nums[l] + nums[r] + nums[i] < 0){
  16. ++l;
  17. }else if (nums[l] + nums[r] + nums[i] > 0){
  18. --r;
  19. }else {
  20. ArrayList<Integer> list = new ArrayList<>();
  21. list.add(nums[i]);
  22. list.add(nums[l]);
  23. list.add(nums[r]);
  24. ans.add(list);
  25. // 跳过重复解
  26. while (l<r && nums[l] == nums[l+1]){
  27. ++l;
  28. }
  29. ++l;
  30. while (l<r && nums[r] == nums[r-1]){
  31. --r;
  32. }
  33. --r;
  34. }
  35. }
  36. }
  37. return ans;
  38. }

附赠两数之和

  1. public int[] twoSum(int[] nums, int target) {
  2. Map<Integer, Integer> map = new HashMap<>();
  3. for (int i = 0; i < nums.length; i++) {
  4. if (map.containsKey(nums[i])){
  5. return new int[]{map.get(nums[i]),i};
  6. }
  7. map.put(target - nums[i], i);
  8. }
  9. return null;
  10. }

0031.下一个排列.md

关键字 下一个 字典序

实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须 原地 修改,只允许使用额外常数空间。

示例

  1. 输入:nums = [1,2,3]
  2. 输出:[1,3,2]
  1. 输入:nums = [3,2,1]
  2. 输出:[1,2,3]
  1. class Solution{
  2. public static void nextPermutation(int[] nums) {
  3. int n = nums.length;
  4. // 从后往前找第一个变小的数字
  5. int i = n - 2;
  6. while (i >= 0 && nums[i] >= nums[i+1]){
  7. i--;
  8. }
  9. // 除了while循环,有两种情况,一是i==-1了, 二是找到了num[i] < num[i+1]
  10. if (i >= 0){
  11. // 从后往前找第一个比num[i]大的数,与num[i]交换
  12. for (int j = n-1; j > i; j--){
  13. if (nums[j] > nums[i]){
  14. // 找到了
  15. swap(nums, i, j);
  16. break;
  17. }
  18. }
  19. }
  20. // 交换完后,需要把后面一部分整体反转,使后面一部分是最小的
  21. reverse(nums, i+1, n-1);
  22. }
  23. private static void swap(int[] nums, int i, int j){
  24. int t = nums[i];
  25. nums[i] = nums[j];
  26. nums[j] = t;
  27. }
  28. // 反转数组区间[lo, hi]
  29. private static void reverse(int[] nums, int lo, int hi){
  30. while (lo < hi){
  31. swap(nums, lo++, hi--);
  32. }
  33. }
  34. }

0032. 最长有效括号.md

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

思路 : 动态规划

我们定义 dp[_i] 表示以下标 _i* 字符结尾的最长有效括号的长度。因此我们知道以 ‘(’ 结尾的子串对应的 dp 值必定为 0 ,我们只需要求解 ‘)’ 在 dp 数组中对应位置的值。

我们从前往后遍历字符串求解 dp 值:

当s[i] = ‘)’ 的时候, 有两种情况 :

  1. s[i - 1] =‘(’ : 这种情况我们可以推出 : dp[i]=dp[i−2]+2
  2. s[i - 1] =‘)’ : 这种情况时又分两种情况
    1. s[ i - dp[i-1] - 1 ] = ‘(‘ : 这种情况我们可以推出 : dp[i] = dp[i−1] +2 + dp[ idp[i−1] − 2 ]
    2. s[ i - dp[i-1] - 1 ] = ‘)’ : dp[i] = 0
  1. public static int longestValidParentheses(String s) {
  2. int[] dp = new int[s.length()];
  3. for (int i = 0; i < s.length(); i++) {
  4. if (s.charAt(i) == '('){
  5. dp[i] = 0;
  6. }else { // ')'
  7. if (i > 0 && s.charAt(i-1) == '('){
  8. if (i-2>=0)
  9. dp[i] = dp[i-2] + 2;
  10. else
  11. dp[i] = 2;
  12. }else if (i > 0 && s.charAt(i-1) == ')'){ // ....))
  13. // 判断 i-dp[i-1]-1 是否是'('
  14. if (i-dp[i-1]-1 >= 0 && s.charAt(i-dp[i-1]-1) == '('){
  15. if (i-dp[i-1]-2 >= 0)
  16. dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2];
  17. else
  18. dp[i] = dp[i-1] + 2;
  19. }else {
  20. dp[i] = 0;
  21. }
  22. }
  23. }
  24. }
  25. int ans = 0;
  26. for (int i = 0; i < s.length(); i++) {
  27. ans = Math.max(ans, dp[i]);
  28. }
  29. return ans;
  30. }

0033. 搜索旋转排序数组.md

关键字 : 旋转数组 有序数组 二分查找

整数数组 nums 按升序排列,数组中的值 互不相同nums 在预先未知的某个下标 k上进行了 旋转.

例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

  1. public int search(int[] nums, int target) {
  2. int left = 0, right = nums.length - 1;
  3. while (left <= right) {
  4. int mid = (right - left) / 2 + left;
  5. // 找到直接返回
  6. if (nums[mid] == target) {
  7. return mid;
  8. }
  9. // 如果nums[mid] >= nums[left], 说明mid在旋转点的左边
  10. if (nums[mid] >= nums[left]) {
  11. if (nums[mid] > target && target >= nums[left]) { // 去左边找
  12. right = mid - 1;
  13. } else { // 去右边找
  14. left = mid + 1;
  15. }
  16. } else { // 说明mid在旋转点的右边
  17. if (nums[mid] < target && target <= nums[right]) { // 去右边找
  18. left = mid + 1;
  19. } else { // 去左边找
  20. right = mid - 1;
  21. }
  22. }
  23. }
  24. return -1;
  25. }

0041.缺失的第一个正整数.md

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数(严格大于0的数)。

实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

方法一 : 排序 + 二分查找

时间复杂度 O(n log n) + O(log n) = O(n log n) 空间复杂度 O(1)

代码略

方法二 : 将数字交换到它应该在的位置上去

时间复杂度为 O(n) 空间复杂度 O(1)

将 num[i] 移动到原数组下标为 num[i]-1 的地方去

  1. public int firstMissingPositive(int[] nums) {
  2. int len = nums.length;
  3. // 循环一遍, 交换数字到应该在的位置
  4. for (int i = 0; i < len; i++) {
  5. while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
  6. // 满足在指定范围内、并且没有放在正确的位置上,才交换
  7. // 例如:数值 3 应该放在索引 2 的位置上
  8. swap(nums, nums[i] - 1, i);
  9. }
  10. }
  11. // 遍历一遍, 找到第一个位置不符合的元素返回
  12. for (int i = 0; i < len; i++) {
  13. if (nums[i] != i + 1) {
  14. return i + 1;
  15. }
  16. }
  17. // 上面for循环没有返回, 则说明缺失的是最大的那个数 即数组长度 + 1
  18. return len + 1;
  19. }
  20. private void swap(int[] nums, int i, int j) {
  21. int temp = nums[i];
  22. nums[i] = nums[j];
  23. nums[index2] = temp;
  24. }

0042. 接雨水.md

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

算法笔记 - 图1

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

思路 : 对于每一列上积攒的雨水量的计算方法是 : 找出当前列左边的最高处, 和右边的最高处, 取二者较小者就是水高线

因此, 方法一就是暴力求解, 对于每一个格子(列), 分别找它左边右边的最高点, 然后减去它自身的高度, 就是当前格子里的积水量, 然后累加在一起即可.

方法一 : 暴力解法

  1. public int trap1(int[] height) {
  2. int ans = 0;
  3. for (int i = 0; i < height.length-1; i++) {
  4. int leftMaxHi = findMax(height, 0, i);
  5. int rightMaxHi = findMax(height, i+1, height.length-1);
  6. int cur = Math.min(leftMaxHi, rightMaxHi) - height[i];
  7. if (cur>0) ans += cur;
  8. }
  9. return ans;
  10. }
  11. // 在[left, right]区间找最大值
  12. private int findMax(int[] arr, int left, int right) {
  13. int ret = arr[left];
  14. while (left <= right){
  15. ret = Math.max(ret, arr[left]);
  16. left++;
  17. }
  18. return ret;
  19. }

方法二 : 动归

思路与方法一一样, 只是用了两个辅助数组分别记录num[i]左右最高点

  1. public int trap2(int[] height) {
  2. int n = height.length; if (n <= 1) return 0;
  3. int[] leftMax = new int[n];
  4. int[] rightMax = new int[n];
  5. // 记录num[i]左边最大值(包含i)
  6. leftMax[0] = height[0];
  7. for (int i = 1; i < n; i++) {
  8. leftMax[i] = Math.max(leftMax[i-1], height[i]);
  9. }
  10. // 记录num[i]右边最大值(包含i)
  11. rightMax[n -1] = height[n -1];
  12. for (int i = n -2; i >= 0; i--){
  13. rightMax[i] = Math.max(rightMax[i+1], height[i]);
  14. }
  15. // 求结果
  16. int ans = 0;
  17. for (int i = 0; i < n - 1; i++) {
  18. ans += Math.min(leftMax[i], rightMax[i]) - height[i];
  19. }
  20. return ans;
  21. }

0046. 全排列.md

关键字 : 数组 字符串 全排列 回溯 深度优先

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

注 : 如果给定一个字符串, 求字符全排列, 则将字符串转成char数组

  1. public List<List<Integer>> permute(int[] nums) {
  2. int len = nums.length;
  3. boolean[] used = new boolean[len];
  4. List<List<Integer>> ans = new ArrayList<>();
  5. dfs(nums, used, len, 0, ans, new ArrayList<>());
  6. return ans;
  7. }
  8. private void dfs(int[] nums, boolean[] used, int len, int depth, List<List<Integer>> ans, List<Integer> list){
  9. if (len == depth){
  10. ans.add(new ArrayList<>(list));
  11. return;
  12. }
  13. for (int i = 0; i < len; i++) {
  14. if (!used[i]){
  15. list.add(nums[i]);
  16. used[i] = true;
  17. dfs(nums, used, len, depth+1, ans, list);
  18. used[i] = false;
  19. list.remove(Integer.valueOf(nums[i]));
  20. }
  21. }
  22. }

0053. 最大子序和.md

关键字 : 数组 最大子序列和 连续子序列和 动归

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

  1. public int maxSubArray(int[] nums) {
  2. int maxSum = nums[0];
  3. // 记录以num[i]结尾的最大子序列和
  4. for (int i = 1; i < nums.length; i++) {
  5. // 如果以nums[i-1]结尾的最大子序列和>0, 则以nums[i]结尾的最大子序列和累加上上一个, 否则就是它本身
  6. if (nums[i-1] > 0){
  7. nums[i] += nums[i-1];
  8. }
  9. // 更新答案
  10. if (nums[i] > maxSum){
  11. maxSum = nums[i];
  12. }
  13. }
  14. return maxSum;
  15. }

或者 :

f(i)=max{f(i−1)+nums[i],nums[i]}

  1. public int maxSubArray(int[] nums) {
  2. int pre = 0, maxAns = nums[0];
  3. for (int x : nums) {
  4. pre = Math.max(pre + x, x);
  5. maxAns = Math.max(maxAns, pre);
  6. }
  7. return maxAns;
  8. }

0054. 螺旋矩阵.md

关键字 : 螺旋矩阵 顺时针打印 二维数组 模拟

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

思路 : 一层一层从外到里打印, 使用四个变量 r1, r2, c1, c2 分别存储上下左右边界值,从而定义当前最外层。

注意只有在 r1 != r2 时才打印最下一行,也就是在当前最外层的行数大于 1 时才打印最下一行

  1. public List<Integer> printMatrix(int[][] matrix) {
  2. List<Integer> ans = new ArrayList<>();
  3. int r1 = 0, c1 = 0;
  4. int r2 = matrix.length - 1, c2 = matrix[0].length - 1;
  5. while (r1 <= r2 && c1 <= c2) {
  6. // 上
  7. for (int i = c1; i <= c2; i++)
  8. ans.add(matrix[r1][i]);
  9. // 右
  10. for (int i = r1 + 1; i <= r2; i++)
  11. ans.add(matrix[i][c2]);
  12. if (r1 != r2)
  13. // 下
  14. for (int i = c2 - 1; i >= c1; i--)
  15. ans.add(matrix[r2][i]);
  16. if (c1 != c2)
  17. // 左
  18. for (int i = r2 - 1; i > r1; i--)
  19. ans.add(matrix[i][c1]);
  20. r1++; r2--; c1++; c2--;
  21. }
  22. return ans;
  23. }

0056.合并区间.md

关键词 : 数组 合并 区间合并 排序

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

  1. 输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
  2. 输出:[[1,6],[8,10],[15,18]]
  1. public int[][] merge(int[][] intervals) {
  2. if (intervals.length == 0) return new int[0][2];
  3. // 按照区间第一位排序
  4. Arrays.sort(intervals, (interval1, interval2) -> interval1[0] - interval2[0]);
  5. List<int[]> ans = new ArrayList<int[]>();
  6. for (int[] interval : intervals) {
  7. int L = interval[0], R = interval[1];
  8. // 列表为空或者列表中最后一个区间右边界小于当前区间左边界,则表示不能与列表中的区间合并,新添加一个区间
  9. if (ans.size() == 0 || ans.get(ans.size() - 1)[1] < L) {
  10. ans.add(new int[]{L, R});
  11. } else { // 否则当前区间可以与列表中最后一个区间合并
  12. ans.get(ans.size() - 1)[1] = Math.max(ans.get(ans.size() - 1)[1], R);
  13. }
  14. }
  15. return ans.toArray(new int[ans.size()][]);
  16. }

0069.x的平方根.md

关键词 : 开方 平方根 二分

实现 sqrt(int x)

  1. public int mySqrt(int x) {
  2. int l = 0, r = x, ans = -1;
  3. while (l <= r) {
  4. int mid = l + (r - l) / 2;
  5. if ((long) mid * mid <= x) {
  6. ans = mid;
  7. l = mid + 1;
  8. } else {
  9. r = mid - 1;
  10. }
  11. }
  12. return ans;
  13. }

0076.最小覆盖子串.md

关键词 : 字符串 子串 最短覆盖子串 滑动窗口

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

  1. 输入:s = "ADOBECODEBANC", t = "ABC"
  2. 输出:"BANC"
  1. public String minWindow(String s, String t) {
  2. if (s.length() == 0 || t.length() == 0 || s.length() < t.length()){
  3. return "";
  4. }
  5. int[] need = new int[128];
  6. //记录需要的字符的个数
  7. for (int i = 0; i < t.length(); i++) {
  8. need[t.charAt(i)]++;
  9. }
  10. //l是当前左边界,r是当前右边界,size记录窗口大小,count是需求的字符个数,start是最小覆盖串开始的index
  11. int left = 0, right = 0, windowSize = s.length()+1, needCharCount = t.length(), start = 0;
  12. //遍历所有字符
  13. while (right < s.length()) {
  14. char c = s.charAt(right);
  15. if (need[c] > 0) { //需要字符c
  16. needCharCount--;
  17. }
  18. // 进入窗口的所有字符都加入need数组,如果need[index]<0 , 说明是不需要的冗余字符
  19. need[c]--;//把右边的字符加入窗口
  20. if (needCharCount == 0) {//窗口中已经包含所有字符
  21. while (left < right && need[s.charAt(left)] < 0) {
  22. // 只要遇到冗余字符,则将该字符从窗口中移除,并且left++
  23. need[s.charAt(left)]++;//释放右边移动出窗口的字符
  24. left++;//指针右移
  25. }
  26. // 出了while循环表示窗口无法再缩小了,此时更新答案
  27. if (right - left + 1 < windowSize) {//不能右移时候挑战最小窗口大小,更新最小窗口开始的start
  28. windowSize = right - left + 1;
  29. start = left;//记录下最小值时候的开始位置,最后返回覆盖串时候会用到
  30. }
  31. //更新完答案后,将left继续向左移动一位,使得窗口不满足要求,需要继续right右移寻找新的答案
  32. need[s.charAt(left)]++;
  33. left++;
  34. needCharCount++;
  35. }
  36. right++;
  37. }
  38. return windowSize == s.length()+1 ? "" : s.substring(start, start + windowSize);
  39. }

0093. 复原 IP 地址.md

给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。

  1. 输入:s = "101023"
  2. 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
  1. 输入:s = "0000"
  2. 输出:["0.0.0.0"]
  1. // 回溯
  2. private List<String> ans = new ArrayList<>();
  3. public List<String> isIp(String s) {
  4. int n = s.length();
  5. if (n > 12 || n < 4) return ans;
  6. // 队列
  7. Deque<String> path = new ArrayDeque<>(4);
  8. dfs(s, 0, 4, path);
  9. return ans;
  10. }
  11. private void dfs(String s, int start, int segment, Deque<String> path) {
  12. int n = s.length();
  13. if (start == n && segment == 0) {
  14. StringBuilder sb = new StringBuilder();
  15. for (String str : path) {
  16. sb.append(str).append(".");
  17. }
  18. sb.deleteCharAt(sb.length()-1);
  19. ans.add(sb.toString());
  20. return ;
  21. }
  22. // 最多3位组成一个段, i位当前段的尾部下标
  23. for (int i=start;i<start+3;i++) {
  24. if (i >= n) break;
  25. // 判断剩下的字符串长度是否比要组成的段的最大长度还长,如果是,则当前答案无效,继续下一次循环
  26. if (segment * 3 < n - i) continue;
  27. if (isIpSegment(s, start, i)) {
  28. // 当前段有效,加入队列
  29. String currentSegment = s.substring(start, i + 1);
  30. path.addLast(currentSegment);
  31. // 关键, 下一段从下标为i+1的位置开始,总段数-1
  32. dfs(s, i + 1, segment - 1, path);
  33. // 回溯
  34. path.removeLast();
  35. }
  36. }
  37. }
  38. private boolean isIpSegment(String s, int left, int right) {
  39. // 如果字符串长度大于1, 且第一位是0,则不可为ip段
  40. if (right - left + 1 > 1 && s.charAt(left) == '0') return false;
  41. int res = Integer.parseInt(s.substring(left, right+1));
  42. return res >= 0 && res <= 255;
  43. }

0098.验证是否为二叉搜索树.md

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

中序遍历即可

  1. private int preNum = Integer.MIN_VALUE;
  2. public boolean isValidBST(TreeNode root) {
  3. if (root == null) return true;
  4. boolean left = isValidBST(root.left);
  5. if (!left){
  6. return false;
  7. }
  8. if (root.val <= preNum){ // 不满足中序有序规律
  9. return false;
  10. }
  11. preNum = root.val;
  12. return isValidBST(root.right);
  13. }

0101.对称二叉树.md

关键词 : 二叉树 对称

给定一个二叉树,检查它是否是镜像对称的。

  1. 1
  2. / \
  3. 2 2
  4. / \ / \
  5. 3 4 4 3
  1. public boolean isSymmetric(TreeNode root) {
  2. if (root == null) return true;
  3. return isSymmetric(root.left, root.right);
  4. }
  5. public boolean isSymmetric(TreeNode node1, TreeNode node2) {
  6. if (node1 == null && node2 == null) return true;
  7. if (node1 == null || node2 == null) return false;
  8. // 先序判断值相等
  9. if (node1.val == node2.val){
  10. // 然后判断node1的左孩子是否和node2的右孩子对称
  11. boolean f1 = isSymmetric(node1.left, node2.right);
  12. // 然后判断node1的右孩子是否和node2的左孩子对称
  13. boolean f2 = isSymmetric(node1.right, node2.left);
  14. return f1 && f2;
  15. }else
  16. return false;
  17. }

0104. 二叉树的最大深度.md

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

思路 : DFS, 后序递归

  1. public int maxDepth(TreeNode root) {
  2. if (root == null) return 0;
  3. if (root.left == null && root.right == null )
  4. return 1;
  5. else if (root.left == null){
  6. // 左树为空, 则返回右树高度+1
  7. return maxDepth(root.right)+1;
  8. }else if (root.right == null){
  9. // 右树为空, 则返回左树高度+1
  10. return maxDepth(root.left)+1;
  11. }else {
  12. // 去左右子树高度最大值+1
  13. return Math.max(maxDepth(root.right)+1, maxDepth(root.left)+1);
  14. }
  15. }

0105. 从前序与中序遍历序列构造二叉树.md

关键词 : 二叉树 重建 前序中序

给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。

  1. // 缓存中序遍历数组每个值对应的索引
  2. private Map<Integer, Integer> indexForInOrders = new HashMap<>();
  3. public TreeNode buildTree(int[] pre, int[] in) {
  4. // 存中序节点索引
  5. for (int i = 0; i < in.length; i++)
  6. indexForInOrders.put(in[i], i);
  7. return buildTree(pre, 0, pre.length - 1, 0);
  8. }
  9. // 只需要前序,前序中当前子树的左右范围,中序左右范围
  10. private TreeNode buildTree(int[] pre, int preL, int preR, int inL) {
  11. if (preL > preR)
  12. return null;
  13. // 当前子树的根
  14. TreeNode root = new TreeNode(pre[preL]);
  15. // 从中序中找出根的位置
  16. int inIndex = indexForInOrders.get(root.val);
  17. // 当前子树中左子树的节点数量
  18. int leftTreeSize = inIndex - inL;
  19. // 递归
  20. root.left = buildTree(pre, preL + 1, preL + leftTreeSize, inL);
  21. root.right = buildTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1);
  22. return root;
  23. }

0106. 从中序与后序遍历序列构造二叉树.md

关键词 : 二叉树 重建 后序中序

根据一棵树的中序遍历与后序遍历构造二叉树。

  1. // 缓存中序遍历数组每个值对应的索引
  2. HashMap<Integer,Integer> indexForInOrders = new HashMap<>();
  3. public TreeNode buildTree(int[] inorder, int[] postorder) {
  4. // 存中序节点索引
  5. for(int i = 0;i < inorder.length; i++)
  6. indexForInOrders.put(inorder[i], i);
  7. return buildTree(postorder ,0, 0, postorder.length - 1);
  8. }
  9. public TreeNode buildTree(int[] postorder, int inL, int postL, int postR) {
  10. if(postR < postL) return null;
  11. // 当前子树的根
  12. TreeNode root = new TreeNode(postorder[postR]);
  13. // 从中序中找出根的位置
  14. int inIndex = indexForInOrders.get(root.val);
  15. root.left = buildTree(postorder ,inL, postL, postL + inIndex - inL - 1);
  16. root.right = buildTree(postorder ,inIndex + 1, postL + inIndex - inL, postR - 1);
  17. return root;
  18. }

0110. 平衡二叉树.md

给定一个二叉树,判断它是否是高度平衡的二叉树。

思路 : 后序递归

  1. public boolean isBalanced(TreeNode root) {
  2. int i = treeHeight(root);
  3. return i != -1;
  4. }
  5. private int treeHeight(TreeNode node){
  6. if (node == null) return 0;
  7. // 计算左子树的高度
  8. int leftHeight = treeHeight(node.left);
  9. // 如果左子树高度为-1, 说明左子树不平衡
  10. if (leftHeight == -1) return -1;
  11. // 计算右子树的高度
  12. int rightHeight = treeHeight(node.right);
  13. // 如果右子树高度为-1, 说明右子树不平衡
  14. if (rightHeight == -1) return -1;
  15. // 如果左子树右子树的高度差不超过1, 则返回最大高度, 超过1则返回-1表示不平衡
  16. return Math.abs(leftHeight-rightHeight)<=1 ? Math.max(leftHeight, rightHeight)+1 : -1;
  17. }

0111. 二叉树的最小深度.md

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

  1. // BFS
  2. public int minDepth(TreeNode root) {
  3. if (root == null) return 0;
  4. Deque<TreeNode> queue = new ArrayDeque<>();
  5. queue.addLast(root);
  6. // 初试深度设为1
  7. int depth = 1;
  8. while (!queue.isEmpty()){
  9. // 当前所在层有多少个节点
  10. int curLevelNodeNum = queue.size();;
  11. for (int i = 0; i < curLevelNodeNum; i++) { // 将一层节点全部出完之后, 深度+1
  12. TreeNode node = queue.removeFirst();
  13. if (node.left == null && node.right == null) return depth;
  14. if (node.left != null) queue.addLast(node.left);
  15. if (node.right != null) queue.addLast(node.right);
  16. }
  17. // 出完一层, 深度++
  18. depth++;
  19. }
  20. return depth;
  21. }

0112.路径总和.md

关键字 : 二叉树 路径总和 递归

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。

  1. public boolean hasPathSum(TreeNode root, int sum) {
  2. if (root == null) {
  3. return false;
  4. }
  5. if (root.left == null && root.right == null) {
  6. return sum == root.val;
  7. }
  8. return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
  9. }

扩展, 找出所有路径总和=targetSum 的路径

  1. List<List<Integer>> ans = new ArrayList<>();
  2. Deque<Integer> path = new LinkedList<>();
  3. public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
  4. dfs(root, targetSum);
  5. return ans;
  6. }
  7. public void dfs(TreeNode root, int targetSum) {
  8. if (root == null) {
  9. return;
  10. }
  11. path.addLast(root.val);
  12. targetSum -= root.val; // targetSum - 当前节点值
  13. // 到达叶子结点, 且targetSum减到0, 添加一个答案
  14. if (root.left == null && root.right == null && targetSum == 0) {
  15. ans.add(new ArrayList<Integer>(path));
  16. }
  17. dfs(root.left, targetSum);
  18. dfs(root.right, targetSum);
  19. //
  20. path.removeLast();
  21. }

0121. 买卖股票的最佳时机.md

关键字 : 股票 最佳时机 买卖一次 最大收益 最大利润 动归

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

思路 : 使用一个变量记录 prices[i] 之前的最低价 minPrice, 每次使用 prices[i] - minPrice, 如果收益更多, 则更新收益

  1. public int maxProfit(int[] prices) {
  2. int ans = 0;
  3. int minPrice = prices[0];
  4. for(int i=1;i <prices.length;i++){
  5. ans = Math.max(ans, prices[i]-minPrice);
  6. minPrice = Math.min(minPrice, prices[i]);
  7. }
  8. return ans;
  9. }

0122. 买卖股票的最佳时机 II.md

关键字 : 股票 最佳时机 买卖多次 最大收益 最大利润 贪心

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

  1. public int maxProfit(int[] prices) {
  2. int ans = 0;
  3. int curMin = prices[0];
  4. for (int i = 1; i < prices.length; i++) {
  5. curMin = Math.min(curMin, prices[i]);
  6. // 只要赚钱就卖掉
  7. if (prices[i] > curMin){
  8. // 卖掉
  9. ans += prices[i] - curMin;
  10. curMin = prices[i];
  11. }
  12. }
  13. return ans;
  14. }

0124. 二叉树中的最大路径和.md

关键词 : 二叉树 最大路径和 关键路径 后序递归

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

算法笔记 - 图2

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

思路, 后序递归, 将dfs(TreeNode node)定义为以node为根的二叉树且路径必须经过node节点的路径最大和

  1. private int maxAns = Integer.MIN_VALUE;
  2. public int maxPathSum(TreeNode root) {
  3. dfs(root);
  4. return maxAns;
  5. }
  6. // 后序递归
  7. private int dfs(TreeNode node) {
  8. if (node == null) return 0;
  9. int left = Math.max(0, dfs(node.left));
  10. int right = Math.max(0, dfs(node.right));
  11. // 三种情况
  12. // 1. 路径为左边+当前节点
  13. // 2. 路径为右边+当前节点
  14. // 3. 路径为左边+当前节点+右边
  15. int sum = node.val + left + right;
  16. // 更新答案
  17. maxAns = Math.max(maxAns, sum);
  18. // 返回上一层的结果只能是选择当前节点左右两边的其中一边
  19. return node.val + Math.max(left, right);
  20. }

0129. 求根节点到叶节点数字之和.md

给你一个二叉树的根节点 root ,树中每个节点都存放有一个 09 之间的数字。

每条从根节点到叶节点的路径都代表一个数字:

算法笔记 - 图3

  1. 输入:root = [4,9,0,5,1]
  2. 输出:495 + 491 + 40 = 1026
  1. public int sumNumbers(TreeNode root) {
  2. return dfs(root, 0);
  3. }
  4. public int dfs(TreeNode node, int prefix) {
  5. if (root == null) {
  6. return 0;
  7. }
  8. int newPrefix = prefix * 10 + node.val;
  9. if (node.left == null && node.right == null) {
  10. return newPrefix;
  11. } else {
  12. return dfs(node.left, newPrefix) + dfs(node.right, newPrefix);
  13. }
  14. }

0141. 环形链表.md

关键字 : 判断链表是否有环, 找链表入环节点

给定一个链表,判断链表中是否有环。

  1. // 快慢指针
  2. public boolean hasCycle(ListNode head) {
  3. if (head == null || head.next == null) return false;
  4. // 至少两个节点
  5. ListNode slow = head;
  6. ListNode fast = head;
  7. while (true){
  8. if (fast.next == null || fast.next.next == null)
  9. return false;
  10. else
  11. fast = fast.next.next;
  12. if (slow.val == fast.val)
  13. return true;
  14. else
  15. slow = slow.next;
  16. }
  17. }
  1. 找出入环节点
  1. public ListNode detectCycle(ListNode head) {
  2. ListNode fast = head, slow = head;
  3. while (true) {
  4. if (fast == null || fast.next == null) return null;
  5. fast = fast.next.next;
  6. slow = slow.next;
  7. if (fast == slow) break;
  8. }
  9. fast = head;
  10. while (slow != fast) {
  11. slow = slow.next;
  12. fast = fast.next;
  13. }
  14. return fast;
  15. }

公式推导

算法笔记 - 图4

任意时刻,fast 指针走过的距离都为 slow 指针的 2 倍。因此,我们有

a+(n+1)b+nc=2(a+b) ⟹ a=c+(n-1)(b+c)
a+(n+1)b+nc=2(a+b) ⟹ a=c+(n−1)(b+c)

我们会发现:从相遇点到入环点的距离加上 n−1 圈的环长,恰好等于从链表头部到入环点的距离。

因此,当发现slow 与 fast 相遇时,我们再额外使用一个指针 ptr。起始,它指向链表头部;随后,它和slow 每次向后移动一个位置。最终,它们会在入环点相遇。

0143. 重排链表.md

关键字 : 链表 重排序

对链表首尾重排

算法笔记 - 图5

  1. 输入: head = [1,2,3,4,5]
  2. 输出: [1,5,2,4,3]

方法一 : 使用List存下每一个节点, 按下标重新连接链表 空间O(n)

方法二 : 快慢指针找中点 + 反转后半段链表 + 合并前后两段链表 空间O(1)

  1. public void reorderList(ListNode head) {
  2. if (head == null || head.next == null) return;
  3. ListNode preSlow = new ListNode(-1, head);
  4. // 快慢指针找中点
  5. ListNode fast = head;
  6. ListNode slow = head;
  7. while (fast != null && fast.next !=null){
  8. fast = fast.next.next;
  9. slow = slow.next;
  10. preSlow = preSlow.next;
  11. }
  12. preSlow.next = null;
  13. // 反转链表后半段
  14. ListNode head2 = reverseList(slow);
  15. // 合并两条链表
  16. merge(head, head2);
  17. // System.out.println(head);
  18. }
  19. // 合并两条链表, 返回合并后的头结点
  20. private void merge(ListNode head1, ListNode head2){
  21. ListNode p = new ListNode(-1);
  22. ListNode x = p;
  23. while (head1 != null && head2 != null){
  24. p.next = head1;
  25. head1 = head1.next;
  26. p = p.next;
  27. p.next = head2;
  28. head2 = head2.next;
  29. p = p.next;
  30. }
  31. if (head1 != null){
  32. p.next = head1;
  33. }
  34. }
  35. // 非递归反转链表, 空间O(1)
  36. public ListNode reverseList(ListNode node) {
  37. if (node == null || node.next == null) return node;
  38. ListNode pre = null;
  39. ListNode cur = node;
  40. while (cur != null) {
  41. ListNode temp = cur.next;
  42. cur.next = pre;
  43. pre = cur;
  44. cur = temp;
  45. }
  46. return pre;
  47. }
  48. // 递归反转链表,返回翻转后的头结点
  49. private ListNode reverseList(ListNode node){
  50. if (node == null || node.next == null) return node;
  51. ListNode node1 = reverseList(node.next);
  52. node.next.next = node;
  53. node.next = null;
  54. return node1;
  55. }

0146.手撕LRU.md

设计和实现一个 LRU (最近最少使用) 缓存机制

实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

要求:在 O(1) 时间复杂度内完成 get() 和 put() 操作?

  1. class LRUCache {
  2. // 使用双向链表记录头尾
  3. static class DLinkedNode {
  4. int key;
  5. int value;
  6. DLinkedNode prev;
  7. DLinkedNode next;
  8. public DLinkedNode() {}
  9. public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
  10. }
  11. // 虚头虚尾
  12. DLinkedNode dummyHead;
  13. DLinkedNode dummyTail;
  14. // 为了快速查询,使用map去记录缓存中所有数据在链表中的位置
  15. Map<Integer, DLinkedNode> map;
  16. // 当前缓存中数据量
  17. int size;
  18. // 缓存容量
  19. int capacity;
  20. // 构造函数
  21. public LRUCache(int capacity) {
  22. // 初始化双向链表
  23. dummyHead = new DLinkedNode();
  24. dummyTail = new DLinkedNode();
  25. dummyHead.next = dummyTail;
  26. dummyTail.prev = dummyHead;
  27. size = 0;
  28. this.capacity = capacity;
  29. map = new HashMap<>(capacity);
  30. }
  31. // 访问缓存, 需要将被访问的元素移到队首
  32. public int get(int key) {
  33. if (map.containsKey(key)) {
  34. int ret = map.get(key).value;
  35. // 当前节点移到链首
  36. moveElementToFirst(map.get(key));
  37. return 1;
  38. }
  39. else
  40. return -1;
  41. }
  42. // 添加元素到缓存
  43. public void put(int key, int value) {
  44. if (map.containsKey(key)){
  45. // 存在, 只修改值
  46. map.get(key).value = value;
  47. }else {
  48. // 不存在, 需新增
  49. if (size == capacity){ // 需要扩容
  50. // 移除链尾元素
  51. // 删除map中对应元素
  52. map.remove(dummyTail.prev.key);
  53. dummyTail.prev = dummyTail.prev.prev;
  54. dummyTail.prev.next = dummyTail;
  55. size--;
  56. }
  57. addElement(key, value);
  58. }
  59. }
  60. public void moveElementToFirst(DLinkedNode node){
  61. // 当前节点从链表上脱离
  62. node.prev.next = node.next;
  63. node.next.prev = node.prev;
  64. // 查到虚头节点后面
  65. node.next = dummyHead.next;
  66. dummyHead.next.prev = node;
  67. node.prev = dummyHead;
  68. dummyHead.next = node;
  69. }
  70. public void addElement(int key, int value){
  71. DLinkedNode node = new DLinkedNode(key, value);
  72. // 节点存入map
  73. map.put(key, node);
  74. // 将节点放到链表最前面
  75. node.next = dummyHead.next;
  76. dummyHead.next.prev = node;
  77. node.prev = dummyHead;
  78. dummyHead.next = node;
  79. size++;
  80. }
  81. }

0148.链表排序.md

关键字 : 链表排序 归并排序 堆排序 优先队列

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

O(n log n) 时间复杂度下,对链表进行排序

方法一 : 优先队列 堆排序

  1. // 时间复杂度 O(n log n) 空间 O(n)
  2. public ListNode sortList(ListNode head) {
  3. ListNode dummyHead = new ListNode(-1);
  4. PriorityQueue<ListNode> priorityQueue = new PriorityQueue<>((o1, o2) -> {
  5. return o1.val - o2.val;
  6. });
  7. while (head != null){
  8. priorityQueue.add(head);
  9. head = head.next;
  10. }
  11. ListNode p = dummyHead;
  12. while (!priorityQueue.isEmpty()){
  13. p.next = priorityQueue.remove();
  14. p = p.next;
  15. }
  16. p.next = null;
  17. return dummyHead.next;
  18. }

方法二 : 归并排序

  1. // // 时间复杂度 O(n log n) 空间 O(log n) (递归栈的)
  2. private ListNode mergeSort(ListNode head) {
  3. if (head == null || head.next == null) return head;
  4. //利用快慢指针来找到链表的中点
  5. ListNode fast = head;
  6. ListNode slow = head;
  7. while (fast.next != null && fast.next.next != null) {
  8. fast = fast.next.next;
  9. slow = slow.next;
  10. }
  11. // 归并右边
  12. ListNode right = mergeSort(slow.next);
  13. slow.next = null; // 链表从中间断开
  14. // 归并左边
  15. ListNode left = mergeSort(head);
  16. return mergeTwoLists(left, right);
  17. }
  18. public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
  19. ListNode dummyHead = new ListNode(0), cur = dummyHead;
  20. while(l1 != null && l2 != null) {
  21. if(l1.val < l2.val) {
  22. cur.next = l1;
  23. l1 = l1.next;
  24. }
  25. else {
  26. cur.next = l2;
  27. l2 = l2.next;
  28. }
  29. cur = cur.next;
  30. }
  31. cur.next = l1 != null ? l1 : l2;
  32. return dummyHead.next;
  33. }

0155. 最小栈.md

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素

  1. class MinStack {
  2. Deque<Integer> dataStack;
  3. Deque<Integer> minStack;
  4. /** initialize your data structure here. */
  5. public MinStack() {
  6. dataStack = new ArrayDeque<>();
  7. minStack = new ArrayDeque<>();
  8. }
  9. public void push(int x) {
  10. dataStack.addFirst(x);
  11. if (minStack.isEmpty() || minStack.peekFirst() >= x){
  12. minStack.addFirst(x);
  13. }
  14. }
  15. public void pop() {
  16. int popNum = dataStack.removeFirst();
  17. if (!minStack.isEmpty() && popNum == minStack.peekFirst()){
  18. minStack.removeFirst();
  19. }
  20. }
  21. public int top() {
  22. if (dataStack.isEmpty())
  23. throw new RuntimeException("栈空");
  24. return dataStack.peekFirst();
  25. }
  26. public int min() {
  27. if (minStack.isEmpty())
  28. throw new RuntimeException("栈空");
  29. return minStack.peekFirst();
  30. }
  31. }

0160. 相交链表.md

关键字 : 链表 相交

找出链表相交的起始节点, 如果没有交点 返回null

  1. public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
  2. ListNode A = headA, B = headB;
  3. while (A != B) {
  4. if( A != null) A = A.next; else A = headB;
  5. if( B != null) B = B.next; else B = headA;
  6. }
  7. return A;
  8. }

如果只要判断两链表是否相交, 只要判断最后一个尾结点地址是否相同即可

  1. public boolean getIntersectionNode(ListNode headA, ListNode headB) {
  2. while (headA.next != null){
  3. headA = headA.next;
  4. }
  5. while (headB.next != null){
  6. headB = headB.next;
  7. }
  8. return headA == headB;
  9. }

0162. 寻找峰值.md

关键字 : 数组 峰值

峰值元素是指其值大于左右相邻值的元素。

给你一个输入数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以实现时间复杂度为 O(logN) 的解决方案吗?

示例

输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。

  1. // 递归二分
  2. public class Solution {
  3. public int findPeakElement(int[] nums) {
  4. return search(nums, 0, nums.length - 1);
  5. }
  6. public int search(int[] nums, int l, int r) {
  7. if (l == r)
  8. return l;
  9. int mid = (l + r) / 2;
  10. if (nums[mid] > nums[mid + 1])
  11. return search(nums, l, mid);
  12. return search(nums, mid + 1, r);
  13. }
  14. }
  1. // 迭代二分
  2. public class Solution {
  3. public int findPeakElement(int[] nums) {
  4. int l = 0, r = nums.length - 1;
  5. while (l < r) {
  6. int mid = (l + r) / 2;
  7. if (nums[mid] > nums[mid + 1])
  8. r = mid;
  9. else
  10. l = mid + 1;
  11. }
  12. return l;
  13. }
  14. }

0199.二叉树的右视图.md

关键字 : 二叉树 右侧 层序遍历 BFS

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例:

算法笔记 - 图6

输入: [1,2,3,null,5,null,4]
输出: [1,3,4]

  1. public List<Integer> rightSideView(TreeNode root) {
  2. List<Integer> ans = new ArrayList<>();
  3. if (root == null) return ans;
  4. Deque<TreeNode> queue = new LinkedList<>();
  5. queue.addLast(root);
  6. while (!queue.isEmpty()){
  7. int size = queue.size();
  8. for (int i = 0; i < size; i++) {
  9. TreeNode node = queue.removeFirst();
  10. if (i == size-1){
  11. // 当前层的最后一个
  12. ans.add(node.val);
  13. }
  14. if (node.left != null) queue.addLast(node.left);
  15. if (node.right != null) queue.addLast(node.right);
  16. }
  17. }
  18. return ans;
  19. }

0200.岛屿数量.md

关键字 : 岛屿 二维数组 回溯

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

输入:grid = [
[“1”,”1”,”0”,”0”,”0”],
[“1”,”1”,”0”,”0”,”0”],
[“0”,”0”,”1”,”0”,”0”],
[“0”,”0”,”0”,”1”,”1”]
]
输出:3

  1. public int numIslands(char[][] grid) {
  2. vis = new boolean[grid.length][grid[0].length];
  3. // 以每一个坐标作为起点坐标
  4. for (int i = 0; i < grid.length; i++) {
  5. for (int j = 0; j < grid[0].length; j++) {
  6. if (grid[i][j] == '1' && !vis[i][j]){
  7. vis[i][j] = true;
  8. ans++;
  9. dfs(grid, i, j);
  10. }
  11. }
  12. }
  13. return ans;
  14. }
  15. // 访问数组
  16. public boolean[][] vis;
  17. // 行走方向
  18. public int[] xDirection = {1, 0, 0, -1};
  19. public int[] yDirection = {0, -1, 1, 0};
  20. public int ans = 0;
  21. public void dfs(char[][] grid, int row, int col){
  22. for (int i = 0; i < 4; i++) {
  23. int nextX = row+xDirection[i];
  24. int nextY = col+yDirection[i];
  25. if (!(nextX >= grid.length || nextX < 0 || nextY >= grid[0].length || nextY < 0) && !vis[nextX][nextY] && grid[nextX][nextY] == '1') {
  26. vis[nextX][nextY] = true;
  27. dfs(grid, nextX, nextY);
  28. }
  29. }
  30. }

0206.反转链表.md

关键字 : 链表 反转

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

方法一 : 递归, 空间O(n)

  1. public ListNode reverseList(ListNode head) {
  2. if (head == null || head.next == null){
  3. return head;
  4. }
  5. // 返回了以head.next为头结点的反转后的链表头
  6. ListNode rev = reverseList(head.next);
  7. // 对当前节点进行反转操作
  8. head.next.next = head;
  9. head.next = null;
  10. return rev;
  11. }

方法二 : 用三个指针, 空间O(1)

  1. // 三指针
  2. public static ListNode reverseList(ListNode head) {
  3. if (head == null || head.next == null) return head;
  4. ListNode p1 = null;
  5. ListNode p2 = head;
  6. ListNode p3 = head.next;
  7. while (p3 != null){
  8. p2.next = p1;
  9. p1 = p2;
  10. p2 = p3;
  11. p3 =p3.next;
  12. }
  13. p2.next = p1;
  14. return p2;
  15. }

0208. 实现 Trie (前缀树).md

关键词 : 前缀树 字典树 数据结构 单词搜索 字典搜索

0221. 最大正方形.md

在一个由 '0''1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

算法笔记 - 图7

输入:matrix = [[“1”,”0”,”1”,”0”,”0”],[“1”,”0”,”1”,”1”,”1”],[“1”,”1”,”1”,”1”,”1”],[“1”,”0”,”0”,”1”,”0”]]
输出:4

思路 : 动态规划

我们用 dp(i,j) 表示以 (i,j) 为右下角,且只包含 11 的正方形的边长最大值。如果我们能计算出所有 dp(i,j) 的值,那么其中的最大值即为矩阵中只包含 11 的正方形的边长最大值,其平方即为最大正方形的面积。

对于每个位置 (i,j),检查在矩阵中该位置的值:

  1. 如果该位置的值是 0,则 dp(i,j)=0,因为当前位置不可能在由 1 组成的正方形中;
  2. 如果该位置的值是 1,则 dp(i,j) 的值由其上方、左方和左上方的三个相邻位置的 dp 值决定。具体而言,当前位置的元素值等于三个相邻位置的元素中的最小值加 1,状态转移方程如下:dp(i,j)=m_in[ dp(_i−1,j), dp(i−1,j−1), dp(i,j−1) ]+1
  1. public int maximalSquare(char[][] matrix) {
  2. int maxSide = 0;
  3. if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
  4. return maxSide;
  5. }
  6. int rows = matrix.length, columns = matrix[0].length;
  7. int[][] dp = new int[rows][columns];
  8. for (int i = 0; i < rows; i++) {
  9. for (int j = 0; j < columns; j++) {
  10. if (matrix[i][j] == '1') {
  11. if (i == 0 || j == 0) {
  12. dp[i][j] = 1;
  13. } else {
  14. dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
  15. }
  16. maxSide = Math.max(maxSide, dp[i][j]);
  17. }
  18. }
  19. }
  20. int maxSquare = maxSide * maxSide;
  21. return maxSquare;
  22. }

0232. 使用栈实现队列.md

使用两个栈实现先入先出队列

  1. class MyQueue {
  2. // 队列长度
  3. private int size;
  4. // 元素入队时入此栈
  5. private Stack<Integer> inQueue;
  6. // 元素出队时从此栈出
  7. private Stack<Integer> outQueue;
  8. public MyQueue() {
  9. inQueue = new Stack<>();
  10. outQueue = new Stack<>();
  11. size=0;
  12. }
  13. /** 将元素x放入队尾 */
  14. public void push(int x) {
  15. inQueue.push(x);
  16. size++;
  17. }
  18. /** 移除队头元素 */
  19. public int pop() {
  20. if (!outQueue.isEmpty()){
  21. size--;
  22. return outQueue.pop();
  23. }else {
  24. // 出栈空
  25. if (inQueue.isEmpty()) // 入栈也空
  26. return -1;
  27. else { // 入栈不空
  28. while (!inQueue.isEmpty()) {
  29. outQueue.push(inQueue.pop());
  30. }
  31. return pop();
  32. }
  33. }
  34. }
  35. /** 查看队头元素 */
  36. public int peek() {
  37. if (!outQueue.isEmpty()){
  38. return outQueue.peek();
  39. }else {
  40. // 出栈空
  41. if (inQueue.isEmpty()) // 入栈也空
  42. return -1;
  43. else { // 入栈不空
  44. while (!inQueue.isEmpty()) {
  45. outQueue.push(inQueue.pop());
  46. }
  47. return peek();
  48. }
  49. }
  50. }
  51. /** 判断队列是否为空 */
  52. public boolean empty() {
  53. return size == 0;
  54. }
  55. }

0236. 二叉树的最近公共祖先.md

关键字 : 二叉树 最近公共祖先 先序遍历 深度优先 DFS

  1. public TreeNode lowestCommonAncestor(TreeNode node, TreeNode p, TreeNode q) {
  2. if (node == null || node == p || node == q)
  3. return node;
  4. // 从左子树中找, 找到其中一个节点就返回, 没找到返回null
  5. TreeNode left = lowestCommonAncestor(node.left, p, q);
  6. // 从右子树中找, 找到其中一个节点就返回, 没找到返回null
  7. TreeNode right = lowestCommonAncestor(node.right, p, q);
  8. // 左右都没找到
  9. if (left == null && right == null)
  10. return null;
  11. // 左边和右边都有, 说明当前node左右各一个,就是祖先节点
  12. else if (left != null && right != null)
  13. return node;
  14. // 否则说明p或q其中一个就是公共祖先
  15. else
  16. return left!=null?left:right;
  17. }

0260. 只出现一次的数字 III.md

关键词 数组 只出现一次的数 位运算 分组异或

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

  1. 输入:nums = [1,2,1,3,2,5]
  2. 输出:[3,5]
  3. 解释:[5, 3] 也是有效的答案。

思路, 分组异或

  1. public static int[] singleNumber(int[] nums) {
  2. // 1. 先对数组所有数进行异或操作, 最终结果就是两个单独的数做异或的结果
  3. int temp = 0;
  4. for (int num : nums) {
  5. temp = temp ^ num;
  6. }
  7. // 2. 从右到左对异或结果temp找第一个不为0的位置
  8. int x = 0;
  9. for (int i = 0; i < 32; i++) {
  10. if ((temp>>i & 1) == 1){
  11. x = i;
  12. break;
  13. }
  14. }
  15. // 下面的x代表只有第x位不为0,其余位都是0的二进制数
  16. x = 1<<x;
  17. // 3. 将整个数组里的数分为第x位是0和第x位是1的两部分, 分别异或, 最终答案即为异或结果
  18. int num1 = 0, num2 = 0;
  19. for (int num : nums) {
  20. if ((num & x) == 0){
  21. num1 = num1 ^ num;
  22. }else {
  23. num2 = num2 ^ num;
  24. }
  25. }
  26. return new int[]{num1, num2};
  27. }

0300.最长递增序列.md

关键字 : 数组 最长不连续递增子序列 最长上升子序列 动归

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

思路 : dp[i] 数组表示以 nums[i] 结尾的最长递增子序列

  1. // 时间复杂度O(n^2)
  2. public static int lengthOfLIS(int[] nums) {
  3. int length = nums.length;
  4. int[] dp = new int[length];
  5. dp[0] = 1;
  6. int maxLen = 1;
  7. for (int i = 1; i < nums.length; i++) {
  8. dp[i] = 1; // 初始化为自己形成子序列
  9. // 遍历前面的,找到比自己小的, 更新dp
  10. for (int j = 0; j < i; j++) {
  11. if (nums[i] > nums[j]){
  12. dp[i] = Math.max(dp[j]+1, dp[i]);
  13. }
  14. }
  15. maxLen = Math.max(maxLen, dp[i]);
  16. }
  17. return maxLen;
  18. }

0328. 奇偶链表.md

关键词 : 链表 奇偶 重排序

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的时间复杂度应为 O(n), 空间复杂度应为 O(1)

  1. 输入: 2->1->3->5->6->4->7->NULL
  2. 输出: 2->3->6->7->1->5->4->NULL
  1. public ListNode oddEvenList(ListNode head) {
  2. if (head == null) {
  3. return head;
  4. }
  5. // 偶数链表头
  6. ListNode head2 = head.next;
  7. // 奇数节点指针头
  8. ListNode odd = head;
  9. // 偶数节点指针
  10. ListNode even = head2;
  11. // 拆分成两个链表
  12. while (even != null && even.next != null) {
  13. odd.next = even.next;
  14. odd = odd.next;
  15. even.next = odd.next;
  16. even = even.next;
  17. }
  18. odd.next = head2;
  19. return head;
  20. }

0334.递增的三元子序列.md

关键字 : 数组 递增 三元 子序列

给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。

如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。

  1. public boolean increasingTriplet(int[] nums) {
  2. int len = nums.length;
  3. if (len < 3) return false;
  4. // 记录下遍历到的最小值和次小值
  5. int min = Integer.MAX_VALUE, mid = Integer.MAX_VALUE;
  6. for (int num : nums) {
  7. if (num <= min) {
  8. min = num;
  9. } else if (num <= mid) { // 出现了比min大且比mid小的数,更新mid
  10. mid = num;
  11. }
  12. else {
  13. // 一定出现了比mid更大的数
  14. return true;
  15. }
  16. }
  17. return false;
  18. }

0415.字符串相加.md

关键字 : 字符串 加法 相加 模拟 进位

给定两个字符串形式的非负整数 num1num2 ,计算它们的和。

提示:

  1. num1 和num2 的长度都小于 5100
  2. num1 和num2 都只包含数字 0-9
  3. num1 和num2 都不包含任何前导零
  4. 你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式
  1. public String addStrings(String num1, String num2) {
  2. int num1Len = num1.length();
  3. int num2Len = num2.length();
  4. int maxLen = Math.max(num1Len, num2Len);
  5. StringBuilder ans = new StringBuilder();
  6. // 进位标识
  7. boolean flag = false;
  8. for (int i = 0; i < maxLen; i++) {
  9. int n1 = 0;
  10. int n2 = 0;
  11. int curProd = 0;
  12. if (i < num1Len)
  13. n1 = num1.charAt(num1Len-i-1)-48;
  14. if (i < num2Len)
  15. n2 = num2.charAt(num2Len-i-1)-48;
  16. curProd = flag?n1+n2+1:n1+n2;
  17. flag = curProd >= 10;
  18. ans.insert(0,curProd%10);
  19. }
  20. // 需要判断一下最终flag是否还有进位,有则最前面插入一个1
  21. return flag?ans.insert(0,1).toString():ans.toString();
  22. }

0704.二分查找.md

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

  1. public int search(int[] nums, int target) {
  2. int left = 0, right = nums.length - 1;
  3. while (left <= right) {
  4. // 防止+操作溢出
  5. int mid = left + (right - left >> 1);
  6. if (nums[mid] == target)
  7. return mid;
  8. else if (nums[mid] < target) {
  9. left = mid + 1;
  10. }
  11. else
  12. right = mid - 1;
  13. }
  14. return -1;
  15. }

0912. 快速排序.md

关键字 : 数组 排序 快排

给你一个整数数组 nums,请你将该数组升序排列。

  1. public void sort(int[] arr, int left, int right){
  2. if (left >= right) return;
  3. int p = partition(arr, left, right);
  4. // 左边排序
  5. sort(arr, left, p-1);
  6. // 右边排序
  7. sort(arr, p+1, right);
  8. }
  9. // 对数组进行分区, 以枢纽为界, 左侧全部小于枢纽, 右侧全部大于枢纽, 返回枢纽最终所在位置
  10. private int partition(int[] arr, int left, int right) {
  11. // 优化点 : 随机选取枢轴元素. 将选中的枢轴元素交换到最左边
  12. int p = left + (new Random()).nextInt(right-left+1);
  13. swap(arr, left, p);
  14. int i = left+1, j = right;
  15. while (true){
  16. // 从左往右找到第一个比枢纽元素大的, 注意循环条件
  17. while (i <= j && arr[i] < arr[left])
  18. i++;
  19. // 从右往左找到第一个比枢纽元素小的,注意循环条件
  20. while (i <= j && arr[j] > arr[left])
  21. j--;
  22. // 循环终止条件
  23. if (i >= j) break;
  24. // 交换两个位置的元素
  25. swap(arr, i, j);
  26. // i往后走一步, j往前走一步, 继续循环
  27. i++;
  28. j--;
  29. }
  30. // 最后交换枢纽元素和j停留的位置
  31. swap(arr, left, j);
  32. return j;
  33. }
  34. private void swap(int[] arr, int i, int j){
  35. int temp = arr[i];
  36. arr[i] = arr[j];
  37. arr[j] = temp;
  38. }

20. 有效的括号.md

关键词 括号匹配 栈

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

思路 , 只要是左括号, 就入栈, 只要是右括号, 就出栈一个, 判断出栈的字符是否与有括号匹配

  1. public boolean isValid(String s) {
  2. HashMap<Character, Character> map = new HashMap<>();
  3. map.put(')', '(');
  4. map.put(']', '[');
  5. map.put('}', '{');
  6. Stack<Character> stack = new Stack<>();
  7. for (int i = 0; i < s.length(); i++) {
  8. Character c = s.charAt(i);
  9. if (c == '(' || c=='[' || c=='{'){
  10. stack.push(c);
  11. }else {
  12. if (stack.isEmpty()){
  13. return false;
  14. }else {
  15. Character popChar = stack.pop();
  16. if (map.get(c) != popChar){
  17. return false;
  18. }
  19. }
  20. }
  21. }
  22. if (!stack.isEmpty()){
  23. return false;
  24. }
  25. return true;
  26. }

234. 回文链表.md

请判断一个链表是否为回文链表。

要求用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题

思路 : 双指针 + 反转后半部分链表

  1. public static boolean isPalindrome1(ListNode head) {
  2. if (head.next == null) return true;
  3. ListNode fast = head;
  4. ListNode slow = head;
  5. while (fast.next != null && fast.next.next != null){
  6. slow = slow.next;
  7. fast = fast.next.next;
  8. }
  9. // 退出while时, slow指向最中间或者中间左边
  10. ListNode reverseNode = slow.next;
  11. slow.next = null;
  12. ListNode reversedHead = reverse(reverseNode);
  13. while (reversedHead != null){
  14. if (reversedHead.val != head.val){
  15. return false;
  16. }
  17. reversedHead = reversedHead.next;
  18. head = head.next;
  19. }
  20. return true;
  21. }
  22. // 三指针反转链表, 空间O(1)
  23. public static ListNode reverse(ListNode head){
  24. if (head == null || head.next == null) return head;
  25. ListNode pre = null;
  26. ListNode cur = head;
  27. ListNode back = null;
  28. while (cur.next != null){
  29. back = cur.next;
  30. cur.next = pre;
  31. pre = cur;
  32. cur = back;
  33. }
  34. cur.next = pre;
  35. return cur;
  36. }

460. LFU 缓存.md

关键词 : 手撕LFU

请你为 最不经常使用(LFU) 缓存算法设计并实现数据结构。

实现 LFUCache 类:

  • LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
  • int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。
  • void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。

思路, 使用两个HashMap, 一个存缓存(key为缓存的key, value为自定义缓存节点), 一个存频次链表(key为频次, value为该频次对应的双向链表), 然后还需要维护一个最低访问频次的int变量, 如果需要移除最低访问频次元素, 就从这个变量对应的频次链表中移除.

  1. class LFUCache {
  2. class Node {
  3. int key;
  4. int value;
  5. int freq;
  6. Node pre;
  7. Node next;
  8. public Node() { }
  9. public Node(int key, int value) {
  10. this.key = key;
  11. this.value = value;
  12. this.freq = 1;
  13. }
  14. }
  15. class DLList {
  16. // 头尾伪结点
  17. Node head;
  18. Node tail;
  19. public DLList() {
  20. head = new Node();
  21. tail = new Node();
  22. head.next = tail;
  23. tail.pre = head;
  24. }
  25. void removeNode(Node node) {
  26. node.pre.next = node.next;
  27. node.next.pre = node.pre;
  28. }
  29. void addHead(Node node) {
  30. node.next = head.next;
  31. head.next.pre = node;
  32. head.next = node;
  33. node.pre = head;
  34. }
  35. }
  36. Map<Integer, Node> map; // 存储缓存, Map的key为缓存的key
  37. Map<Integer, DLList> freqMap; // 每个频次维护一个双向链表
  38. int size; // 当前元素个数
  39. int capacity; // 允许存储的最大元素个数
  40. int minFreq; // 所有元素中的最小访问频次
  41. public LFUCache(int capacity) {
  42. map = new HashMap<>(capacity);
  43. freqMap = new HashMap<>();
  44. this.capacity = capacity;
  45. this.size = 0;
  46. }
  47. public int get(int key) {
  48. Node node = map.get(key);
  49. if (node == null) {
  50. return -1;
  51. }
  52. // 如果存在, 则对应元素访问频次++
  53. updateFreq(node);
  54. return node.value;
  55. }
  56. public void put(int key, int value) {
  57. if (capacity == 0) return;
  58. Node node = map.get(key);
  59. // 如果key已存在, 则更新value, 并更新其访问频次
  60. if (node != null) {
  61. node.value = value;
  62. updateFreq(node);
  63. } else { // key不存在, 则新增key到访问频次为1的链表中
  64. // 检查是否超出capacity, 超出则删除一个访问频次最低的
  65. if (size == capacity) {
  66. DLList minFreqLL = freqMap.get(minFreq);
  67. // 从缓存中删除
  68. map.remove(minFreqLL.tail.pre.key);
  69. // 从频次链表中删除
  70. minFreqLL.removeNode(minFreqLL.tail.pre); // 这里不需要维护min, 因为下面add了newNode后min肯定是1.
  71. size--;
  72. }
  73. // 将新节点插入访问频次为1的频次链表中
  74. node = new Node(key, value);
  75. map.put(key, node);
  76. DLList ll = freqMap.getOrDefault(1, new DLList());
  77. ll.addHead(node);
  78. freqMap.put(1, ll);
  79. size++;
  80. minFreq = 1;
  81. }
  82. }
  83. // node已存在,更新node的访问频次
  84. void updateFreq(Node node) {
  85. // 从原freq对应的链表里移除
  86. int preFreq = node.freq;
  87. DLList ll = freqMap.get(preFreq);
  88. ll.removeNode(node);
  89. // 如果当前频次链表为最低频次链表, 且从链表中删除node后链表为空时, 需要更新最小访问频次变量
  90. if (preFreq == minFreq && ll.head.next == ll.tail) {
  91. minFreq = preFreq + 1;
  92. }
  93. // 加入新freq对应的链表
  94. int curFreq = preFreq + 1;
  95. node.freq = curFreq;
  96. DLList curDLL = freqMap.getOrDefault(curFreq, new DLList());
  97. curDLL.addHead(node);
  98. freqMap.put(curFreq, curDLL);
  99. }
  100. }

470. 用 Rand7() 实现 Rand10().md

已有方法 rand7 可生成 1 到 7 范围内的均匀随机整数,试写一个方法 rand10 生成 1 到 10 范围内的均匀随机整数。

思路 :

执行一次 rand7() , 可得到均匀分布的 1, 2, 3, 4, 5, 6, 7

rand7() - 1, 得到均匀分布的 0, 1, 2, 3, 4, 5, 6

( rand7() - 1 ) * 7, 得到均匀分布的 0, 7, 14, 21, 28, 35, 42

( rand7() - 1 ) * 7 + rand7(), 即( 0, 7, 14, 21, 28, 35, 42 ) + ( 1, 2, 3, 4, 5, 6, 7 ) 可以得到均匀分布的(1,2,3,4, … , 49)

对上面的式子产生的1 - 49 ,只取前40个, 即 1 - 40; 将得到的1 - 40 这个数X对10取模再减1, 即可得到rand10()

即, X % 10 + 1

  1. public int rand10(){
  2. while (true){
  3. int num = (rand7() - 1) * 7 + rand7();
  4. if (num <= 40)
  5. return num % 10 + 1;
  6. rand10();
  7. }
  8. }
  9. public int rand7(){
  10. Random random = new Random();
  11. return random.nextInt(7) + 1;
  12. }

8. 字符串转换整数 (atoi).md

关键字 : 字符串转整数

函数 myAtoi(string s) 的算法如下:

  • 读入字符串并丢弃无用的前导空格
  • 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
  • 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
  • 将前面步骤读入的这些数字转换为整数(即,”123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
  • 如果整数数超过 32 位有符号整数范围 [−231, 231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被固定为 −231 ,大于 231 − 1 的整数应该被固定为 231 − 1 。
    返回整数作为最终结果。
  1. public static int myAtoi(String s) {
  2. s = s.trim();
  3. if (s.length() == 0) return 0;
  4. boolean isNegative = false; // 负数标记
  5. if (s.charAt(0) == '-'){
  6. isNegative = true;
  7. }
  8. long ans = 0;
  9. for (int i = 0 ; i < s.length(); i++) {
  10. // 如果第一个字符是正负号,则跳过
  11. if (i == 0 && (s.charAt(0) == '-' || s.charAt(0) == '+')){
  12. continue;
  13. }
  14. // 判断当前字符是否是数字, 如果是, 则累加, 如果不是, 则结束遍历
  15. int cur = s.charAt(i) - '0';
  16. if (cur >= 0 && cur <= 9){
  17. ans = ans * 10 + cur;
  18. }else {
  19. break;
  20. }
  21. // 判断是否越界
  22. if (isNegative && ans > Integer.MAX_VALUE){
  23. return Integer.MIN_VALUE;
  24. }else if (ans > Integer.MAX_VALUE){
  25. return Integer.MAX_VALUE;
  26. }
  27. }
  28. return isNegative ? -(int) ans : (int) ans;
  29. }

剑指 Offer 15. 二进制中1的个数.md

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数

思路 : n&(n-1) 位运算可以将 n 的位级表示中最低的那一位 1 设置为 0。不断将 1 设置为 0,直到 n 为 0。

  1. public int NumberOf1(int n) {
  2. int cnt = 0;
  3. while (n != 0) {
  4. cnt++;
  5. n &= (n - 1);
  6. }
  7. return cnt;
  8. }

剑指 Offer 41. 数据流中的中位数.md

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

  1. /* 大顶堆,存储左半边元素 */
  2. private PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1);
  3. /* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */
  4. private PriorityQueue<Integer> right = new PriorityQueue<>();
  5. /* 当前数据流读入的元素个数 */
  6. private int N = 0;
  7. public void Insert(Integer val) {
  8. /* 插入要保证两个堆存于平衡状态 */
  9. if (N % 2 == 0) {
  10. /* N 为偶数的情况下插入到右半边。
  11. * 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大,
  12. * 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */
  13. left.add(val);
  14. right.add(left.poll());
  15. } else {
  16. right.add(val);
  17. left.add(right.poll());
  18. }
  19. N++;
  20. }
  21. // 任意时刻获取中位数, 复杂度O(1)
  22. public Double GetMedian() {
  23. if (N % 2 == 0)
  24. return (left.peek() + right.peek()) / 2.0;
  25. else
  26. return (double) right.peek();
  27. }

剑指 Offer 50. 第一个只出现一次的字符.md

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

ASCII 码只有 128 个字符,因此可以使用长度为 128 的整型数组来存储每个字符出现的次数。

注意最后遍历的是字符串, 而不是数组, 因为要保证找到的是第一个出现一次的字符

  1. public static int FirstNotRepeatingChar(String str) {
  2. // 128长度数组存储字符出现的次数
  3. int[] cnts = new int[128];
  4. // 计算每个字符出现的次数
  5. for (int i = 0; i < str.length(); i++)
  6. cnts[str.charAt(i)]++;
  7. // 从头到尾再遍历一遍字符串, 看哪个字符的出现次数等于1
  8. for (int i = 0; i < str.length(); i++)
  9. if (cnts[str.charAt(i)] == 1)
  10. return i;
  11. return -1;
  12. }

剑指 Offer 51. 数组中的逆序对.md

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

思路 : 利用归并排序过程发现逆序对

  1. // 答案
  2. private int res;
  3. public int reversePairs(int[] nums){
  4. res = 0;
  5. mergeSort(nums);
  6. return res;
  7. }
  8. public void mergeSort(int[] arr){
  9. int[] temp = new int[arr.length];
  10. sort(arr, 0, arr.length-1, temp);
  11. }
  12. public void sort(int[] arr, int left, int right, int[] temp){
  13. if (left>=right){
  14. return;
  15. }
  16. int mid = (left+right)/2;
  17. sort(arr, left, mid, temp);
  18. sort(arr, mid+1, right, temp);
  19. if (arr[mid] > arr[mid+1])
  20. merge(arr, left, mid, right, temp);
  21. }
  22. // 归并过程
  23. private void merge(int[] arr, int left, int mid, int right, int[] temp) {
  24. System.arraycopy(arr, left, temp, left, right-left+1);
  25. // 双指针比较并移动
  26. int i = left, j = mid+1;
  27. for (int k = left; k <= right; k++) {
  28. if (i > mid){
  29. arr[k] = temp[j];
  30. j++;
  31. }else if (j > right){
  32. arr[k] = temp[i];
  33. i++;
  34. }else if (temp[i] <= temp[j]){
  35. arr[k] = temp[i];
  36. i++;
  37. }else {
  38. arr[k] = temp[j];
  39. // 最关键的地方! 如果temp[i] > temp[j], 即左边指针位置元素大于右边, 则左边所有剩余元素都与右边指针位置元素形成逆序对
  40. res += (mid-i+1);
  41. j++;
  42. }
  43. }
  44. }

剑指 Offer 59 - I. 滑动窗口的最大值.md

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

方法一 : 优先队列 大顶堆

  1. public ArrayList<Integer> maxInWindows(int[] num, int size) {
  2. ArrayList<Integer> ans = new ArrayList<>();
  3. if (size > num.length || size < 1)
  4. return ans;
  5. // 大顶堆, 堆顶是最大值
  6. PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);
  7. for (int i = 0; i < size; i++)
  8. heap.add(num[i]);
  9. ans.add(heap.peek());
  10. /* 维护一个大小为 size 的大顶堆 */
  11. for (int i = 0, j = i + size; j < num.length; i++, j++) {
  12. // 滑出窗口的数出堆, 新加入窗口的数入堆
  13. heap.remove(num[i]);
  14. heap.add(num[j]);
  15. // 取新窗口中的最大值
  16. ans.add(heap.peek());
  17. }
  18. return ans;
  19. }

方法二 : 单调队列

  1. class Solution {
  2. public int[] maxSlidingWindow(int[] nums, int k) {
  3. if(nums.length == 0 || k == 0) return new int[0];
  4. Deque<Integer> deque = new LinkedList<>();
  5. int[] res = new int[nums.length - k + 1];
  6. for(int j = 0, i = 1 - k; j < nums.length; i++, j++) {
  7. // 删除 deque 中对应的 nums[i-1]
  8. if(i > 0 && deque.peekFirst() == nums[i - 1])
  9. deque.removeFirst();
  10. // 保持 deque 递减
  11. while(!deque.isEmpty() && deque.peekLast() < nums[j])
  12. deque.removeLast();
  13. deque.addLast(nums[j]);
  14. // 记录窗口最大值
  15. if(i >= 0)
  16. res[i] = deque.peekFirst();
  17. }
  18. return res;
  19. }
  20. }

剑指 Offer 66. 构建乘积数组.md

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

  1. 输入: [1,2,3,4,5]
  2. 输出: [120,60,40,30,24]
  1. public int[] constructArr(int[] a) {
  2. int len = a.length;
  3. if(len == 0) return new int[0];
  4. // 声明两个数组, 一个保存A[i]左边所有数的累乘和
  5. int[] dp1 = new int[len];
  6. // 一个保存A[i]右边所有数的累乘和
  7. int[] dp2 = new int[len];
  8. // 累乘A[i]左边的数
  9. dp1[0] = 1;
  10. for (int i = 1; i < len; i++) {
  11. dp1[i] = dp1[i - 1] * a[i - 1];
  12. }
  13. // 累乘A[i]右边的数
  14. dp2[len - 1] = 1;
  15. for (int i = len - 2; i >= 0; i--) {
  16. dp2[i] = dp2[i + 1] * a[i + 1];
  17. }
  18. // 求B[i] = 左边 * 右边
  19. for (int i = 0; i < len; i++) {
  20. a[i] = dp1[i] * dp2[i];
  21. }
  22. return a;
  23. }

剑指 Offer II 001. 整数除法.md

给定两个整数 ab ,求它们的除法的商 a/b ,要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%'

  1. 示例:
  2. 10 / 3 = 3
  3. 7 / -3 = -2
  4. 31 / 4 = 7

思路 :

( 7 / 3 = 2 ) ==> 7 = 3 * 2^1

( 10 / 3 = 3 ) ==> 10 = 3 2^1 + 3 2^0 = 3 * (2^1 + 2^0)

( 31/ 4 = 7 ) ==> 4 2^2 + 4 2^1 + 4 2^0 = 4 ( 2^2 + 2^1 + 2^0 )

  1. public int divide(int dividend, int divisor) {
  2. // 特殊情况处理, 面试时先不写, 一般很难考虑到这一层
  3. if (dividend == Integer.MIN_VALUE && divisor == -1) return Integer.MAX_VALUE;
  4. // 将两数都转为正整数处理
  5. boolean isNegative = (dividend < 0 && divisor > 0) || (dividend > 0 && divisor < 0);
  6. long a = Math.abs((long) dividend);
  7. long b = Math.abs((long) divisor);
  8. int ans = 0;
  9. int shift = 31;
  10. // a 每次减去 b移shift位, 相当于 a - b * 2^shift
  11. while (a >= b){
  12. while (a < b << shift){
  13. shift--;
  14. }
  15. a -= b << shift;
  16. ans += 1 << shift;
  17. }
  18. return isNegative ? -ans : ans;
  19. }

剑指 Offer II 002. 二进制加法.md

给定两个 01 字符串 ab ,请计算它们的和,并以二进制字符串的形式输出。

输入为 非空 字符串且只包含数字 10

  1. 输入: a = "1010", b = "1011"
  2. 输出: "10101"

思路 : 模拟,设置是否要进位的标志变量flag

  1. public static String addBinary(String a, String b) {
  2. // 模拟
  3. int aLen = a.length(), bLen = b.length();
  4. int maxLen = Math.max(aLen, bLen);
  5. boolean flag = false;
  6. StringBuilder ans = new StringBuilder();
  7. for (int i = 0; i < maxLen; i++) {
  8. char aChar = '0';
  9. char bChar = '0';
  10. if (aLen-i-1 >= 0) aChar=a.charAt(aLen-i-1);
  11. if (bLen-i-1 >= 0) bChar=b.charAt(bLen-i-1);
  12. int cur = aChar - '0' + bChar - '0';
  13. if (flag) cur++;
  14. if (cur > 1){ // 要进位了
  15. flag = true;
  16. cur = cur % 2;
  17. }else {
  18. flag = false;
  19. }
  20. ans.insert(0, cur);
  21. }
  22. if (flag) ans.insert(0, '1');
  23. return ans.toString();
  24. }

剑指 Offer II 003. 前 n 个数字二进制中 1 的个数.md

关键词 : 二进制 动态规划

给定一个非负整数 n ,请计算 0n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。

每次计算一个数包含多少个1时,只需要将该数往右移动一位(等价于除2),此时有两种情况:

如果计算的是偶数,1的个数 = 移位后的数中1的个数
如果计算的是奇数,1的个数 = 移位后的数中1的个数 + 1 (因为移位前末尾有个1)

  1. public int[] countBits(int n) {
  2. int[] ret = new int[n+1];
  3. for (int i = 1; i <= n; i++) {
  4. ret[i] = ret[i>>1];
  5. if (i % 2 == 1) ret[i]++;
  6. }
  7. return ret;
  8. }
  1. func countBits(n int) []int {
  2. ret := make([]int, n+1)
  3. for i := 1; i <= n; i++ {
  4. ret[i] = ret[i>>1]
  5. if i%2 == 1 {
  6. ret[i]++
  7. }
  8. }
  9. return ret
  10. }

剑指 Offer II 004. 只出现一次的数字 .md

关键词 : 数组 只出现一次的数 位运算

给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。

思路 : 累加每个数字每一个bit位上的1, 最后对每一位上1的个数对3取余数, 就得到了目标答案在这一位上是0还是1

  1. public int singleNumber(int[] nums) {
  2. int[] bitCounts = new int[32];
  3. // 对每个数字进行位移, 累加每一位上的1
  4. for (int num : nums) {
  5. for (int i = 31; i >= 0; i--) {
  6. if ((num & 1) == 1) {
  7. bitCounts[i]++;
  8. }
  9. num = num >> 1;
  10. }
  11. }
  12. // 遍历记录数组, 每一位对3取余, 得到目标值每一位上的值
  13. int ans = 0;
  14. for (int i = 0; i < 32; i++) {
  15. ans = ans<<1;
  16. ans += bitCounts[i] % 3;
  17. }
  18. return ans;
  19. }

剑指 Offer II 005. 单词长度的最大乘积.md

给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。

  1. 输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]
  2. 输出: 16
  3. 解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。
  4. 输入: words = ["a","aa","aaa","aaaa"]
  5. 输出: 0
  6. 解释: 不存在这样的两个单词。

思路,比较挫,暴力遍历每一个字符串,比较是否有相同字符,如果没有则更新maxProd

时间复杂度 O(n^2 + L1+L2)

  1. public int maxProduct(String[] words) {
  2. int maxProd = 0;
  3. for (int i = 0; i < words.length; i++) {
  4. for (int j = i+1; j < words.length; j++) {
  5. if (!containSameChar(words[i], words[j])){
  6. maxProd = Math.max(maxProd, words[i].length() * words[j].length());
  7. }
  8. }
  9. }
  10. return maxProd;
  11. }
  12. public boolean containSameChar(String word1, String word2){
  13. boolean[] arr = new boolean[26];
  14. for (int i = 0; i < word1.length(); i++) {
  15. arr[word1.charAt(i)-'a'] = true;
  16. }
  17. for (int i = 0; i < word2.length(); i++) {
  18. if (arr[word2.charAt(i) - 'a'])
  19. return true;
  20. }
  21. return false;
  22. }

剑指 Offer II 006. 排序数组中两个数字之和.md

给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target

假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。

输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。

思路 : 双指针, 一头一尾,慢慢向中间靠拢

  1. public int[] twoSum(int[] numbers, int target) {
  2. int index1 = 0, index2 = numbers.length-1;
  3. while (true){
  4. int curSum = numbers[index1] + numbers[index2];
  5. if (curSum == target){
  6. return new int[]{index1 , index2};
  7. }else if (curSum < target){
  8. index1++;
  9. }else {
  10. index2--;
  11. }
  12. }
  13. }

剑指 Offer II 008. 和大于等于 target 的最短子数组.md

关键词 数组 连续子数组 最小长度 滑动窗口

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

思路, 滑动窗口

  1. public int minSubArrayLen1(int s, int[] nums) {
  2. int left = 0, right = 0;
  3. int curSum = 0, ans = Integer.MAX_VALUE;
  4. while (right < nums.length) {
  5. curSum += nums[right++];
  6. while (curSum >= s) {
  7. ans = Math.min(ans, right - left);
  8. curSum -= nums[left++];
  9. }
  10. }
  11. return ans == Integer.MAX_VALUE ? 0 : ans;
  12. }

多线程交替打印.md

  1. public class 多线程交替输出 {
  2. public static void main(String[] args) {
  3. // 推荐使用方法一和三,最简单
  4. method3();
  5. }
  6. // 方法一
  7. public static void method1(){
  8. WaitNotify waitNotify = new WaitNotify(1, 5);
  9. new Thread(()->waitNotify.print("hello1", 1, 2),"t1").start();
  10. new Thread(()->waitNotify.print("hello2", 2, 3),"t2").start();
  11. new Thread(()->waitNotify.print("hello3", 3, 1),"t3").start();
  12. }
  13. // 方法二
  14. public static void method2(){
  15. AwaitSignal awaitSignal = new AwaitSignal(5);
  16. Condition t1 = awaitSignal.newCondition();
  17. Condition t2 = awaitSignal.newCondition();
  18. Condition t3 = awaitSignal.newCondition();
  19. new Thread(()->awaitSignal.print("hello1", t1, t2), "t1").start();
  20. new Thread(()->awaitSignal.print("hello2", t2, t3), "t2").start();
  21. new Thread(()->awaitSignal.print("hello3", t3, t1), "t3").start();
  22. awaitSignal.lock();
  23. try {
  24. t1.signal();
  25. } finally {
  26. awaitSignal.unlock();
  27. }
  28. }
  29. // 方法三
  30. static Thread t1;
  31. static Thread t2;
  32. static Thread t3;
  33. public static void method3(){
  34. ParkUnpark parkUnpark = new ParkUnpark(5);
  35. t1 = new Thread(() -> parkUnpark.print("hello1", t2));
  36. t2 = new Thread(() -> parkUnpark.print("hello2", t3));
  37. t3 = new Thread(() -> parkUnpark.print("hello3", t1));
  38. t1.start();
  39. t2.start();
  40. t3.start();
  41. LockSupport.unpark(t1);
  42. }
  43. }
  44. // 第一种方法, 使用Wait() NotifyAll()
  45. class WaitNotify{
  46. private int flag;
  47. private int loopNumber;
  48. public void print(String str, int flag, int nextFlag){
  49. for (int i = 0; i < this.loopNumber; i++) {
  50. synchronized (this){
  51. while (this.flag != flag){
  52. try {
  53. this.wait();
  54. } catch (InterruptedException e) {
  55. e.printStackTrace();
  56. }
  57. }
  58. System.out.println(Thread.currentThread().getName() + " : " + str);
  59. this.flag = nextFlag;
  60. this.notifyAll();
  61. }
  62. }
  63. }
  64. public WaitNotify(int flag, int loopNumber) {
  65. this.flag = flag;
  66. this.loopNumber = loopNumber;
  67. }
  68. }
  69. // 方法二 : 使用ReentrantLock的Await 和 Signal
  70. class AwaitSignal extends ReentrantLock{
  71. private int loopNumber;
  72. public AwaitSignal(int loopNumber) {
  73. this.loopNumber = loopNumber;
  74. }
  75. public void print(String str, Condition current, Condition next){
  76. for (int i = 0; i < this.loopNumber; i++) {
  77. this.lock();
  78. try {
  79. // 一上来先进阻塞队列
  80. current.await();
  81. System.out.println(Thread.currentThread().getName() + " : " + str);
  82. // 打印完,唤醒下一个
  83. next.signal();
  84. } catch (InterruptedException e) {
  85. e.printStackTrace();
  86. } finally {
  87. this.unlock();
  88. }
  89. }
  90. }
  91. }
  92. // 方法三 : 使用Park Unpark
  93. class ParkUnpark{
  94. private int loopNumber;
  95. public void print(String str, Thread next){
  96. for (int i = 0; i < this.loopNumber; i++) {
  97. LockSupport.park();
  98. System.out.println(Thread.currentThread().getName() + " : " + str);
  99. LockSupport.unpark(next);
  100. }
  101. }
  102. public ParkUnpark(int loopNumber) {
  103. this.loopNumber = loopNumber;
  104. }
  105. }

奇升偶降链表.md

字节跳动高频题——排序奇升偶降链表

给定一个奇数位升序,偶数位降序的链表,将其重新排序。

  1. 输入: 1->8->3->6->5->4->7->2->NULL
  2. 输出: 1->2->3->4->5->6->7->8->NULL

思路 :

  1. 按奇偶位置拆分链表, 得1->3->5->7->NULL和8->6->4->2->NULL
  2. 反转偶链表,得1->3->5->7->NULL和2->4->6->8->NULL
  3. 合并两个有序链表,得1->2->3->4->5->6->7->8->NULL

时间复杂度为O(N),空间复杂度O(1)。

  1. public ListNode oddEvenList(ListNode head) {
  2. if (head == null) {
  3. return head;
  4. }
  5. // 偶数链表头
  6. ListNode head2 = head.next;
  7. // 奇数节点指针头
  8. ListNode odd = head;
  9. // 偶数节点指针
  10. ListNode even = head2;
  11. // 拆分成两个链表
  12. while (even != null && even.next != null) {
  13. odd.next = even.next;
  14. odd = odd.next;
  15. even.next = odd.next;
  16. even = even.next;
  17. }
  18. odd.next = null;
  19. // 反转偶数链表
  20. ListNode newHead2 = reverse(head2);
  21. // 链表合并
  22. return mergeTwoLists(head, newHead2);
  23. }
  24. // 合并两个有序链表
  25. public ListNode mergeTwoLists(ListNode head1, ListNode head2) {
  26. ListNode dummyHead = new ListNode(-1);
  27. // p指针指向合并后的最后一个节点
  28. ListNode p = dummyHead;
  29. while (head1 != null && head2 != null) {
  30. if (head1.val <= head2.val) {
  31. p.next = head1;
  32. head1 = head1.next;
  33. } else {
  34. p.next = head2;
  35. head2 = head2.next;
  36. }
  37. p = p.next;
  38. }
  39. // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
  40. p.next = head1 == null ? head2 : head1;
  41. return dummyHead.next;
  42. }
  43. // 迭代反转链表
  44. private ListNode reverse(ListNode head) {
  45. if (head == null || head.next == null) return head;
  46. ListNode pre = null;
  47. ListNode cur = head;
  48. while (cur != null) {
  49. ListNode temp = cur.next;
  50. cur.next = pre;
  51. pre = cur;
  52. cur = temp;
  53. }
  54. return pre;
  55. }

手撕单例模式.md

懒汉式(DCL : double check lock)

  1. class LazySingleton{
  2. private static volatile LazySingleton instance;
  3. private LazySingleton(){}
  4. public static LazySingleton getInstance() {
  5. if (instance == null){
  6. synchronized (LazySingleton.class){
  7. if (instance == null)
  8. instance = new LazySingleton();
  9. }
  10. }
  11. return instance;
  12. }
  13. }

饿汉式

  1. class HungrySingleton{
  2. private static HungrySingletonTest instance = new HungrySingletonTest();
  3. private HungrySingleton(){}
  4. public static HungrySingletonTest getInstance() {
  5. return instance;
  6. }
  7. }

手撕归并排序.md

  1. public void mergeSort(int[] arr){
  2. // temp是一个辅助归并的空数组
  3. int[] temp = new int[arr.length];
  4. sort(arr, 0, arr.length-1, temp);
  5. }
  6. public void sort(int[] arr, int left, int right, int[] temp){
  7. if (left>=right){
  8. return;
  9. }
  10. int mid = (left+right)/2;
  11. sort(arr, left, mid, temp);
  12. sort(arr, mid+1, right, temp);
  13. // 优化点, 面试中可以后面补充, 如果左边最后一个小于或等于右边第一个则不用归并
  14. if (arr[mid] > arr[mid+1])
  15. merge(arr, left, mid, right, temp);
  16. }
  17. // 归并过程
  18. private void merge(int[] arr, int left, int mid, int right, int[] temp) {
  19. // 归并时, 将原数组中需要归并的部分复制到辅助数组中
  20. System.arraycopy(arr, left, temp, left, right-left+1); // 优化点
  21. // 双指针比较并移动
  22. int i = left, j = mid+1;
  23. for (int k = left; k <= right; k++) {
  24. // 如果左边归并完了, 直接放入右边的元素即可
  25. if (i > mid){
  26. arr[k] = temp[j];
  27. j++;
  28. }else if (j > right){ // 如果右边归并完了, 直接放入左边的元素即可
  29. arr[k] = temp[i];
  30. i++;
  31. }else if (temp[i] <= temp[j]){ // 取二者之中较小
  32. arr[k] = temp[i];
  33. i++;
  34. }else {
  35. arr[k] = temp[j];
  36. j++;
  37. }
  38. }
  39. }

手撕线程池.md

关键在阻塞队列

  1. class ThreadPool{
  2. // 核心线程数
  3. private int coreSize;
  4. // 阻塞队列(任务集合)
  5. private BlockingDeque<Runnable> taskQueue;
  6. private int queueCapacity;
  7. // 线程集合
  8. private HashSet<Worker> workers;
  9. public ThreadPool(int coreSize, int queueCapacity) {
  10. this.coreSize = coreSize;
  11. this.taskQueue = new LinkedBlockingDeque<>(queueCapacity);
  12. this.queueCapacity = queueCapacity;
  13. this.workers = new HashSet<>();
  14. }
  15. // 执行任务
  16. public void execute(Runnable task){
  17. synchronized (workers) {
  18. // 1. 判断当前线程数是否等于coreSize
  19. if (workers.size() < coreSize){
  20. // 若小于, 则创建新线程执行任务
  21. Worker worker = new Worker(task);
  22. worker.start();
  23. // worker 加入线程集合
  24. workers.add(worker);
  25. }else{
  26. // 若等于,则放入阻塞队列
  27. // 情况一: 阻塞队列不满, 直接添加
  28. if (taskQueue.size() < queueCapacity) {
  29. System.out.println("加入任务队列");
  30. taskQueue.add(task);
  31. }
  32. else {
  33. System.out.println("任务被抛弃...");
  34. }
  35. // 情况二: 阻塞队列满, 拒绝策略(啥也不做)
  36. }
  37. }
  38. }
  39. // 线程包装类
  40. class Worker extends Thread{
  41. private Runnable task;
  42. public Worker(Runnable task) {
  43. this.task = task;
  44. }
  45. @Override
  46. public void run() {
  47. // 执行任务
  48. // task 任务执行完毕后, 或者task为空时, 从任务队列中取出任务执行
  49. while (task != null || !taskQueue.isEmpty()){
  50. try {
  51. if (task == null)
  52. task = taskQueue.take();
  53. task.run();
  54. } catch (Exception e) {
  55. e.printStackTrace();
  56. } finally {
  57. task = null;
  58. }
  59. }
  60. // 任务队列中没有任务, 则销毁当前worker
  61. synchronized (workers) {
  62. workers.remove(this);
  63. }
  64. }
  65. }
  66. }