TopK

诸如 找出第K大/小、找出最小/大的K个数问题

  • 方法效率排行

快排 > 堆 > 排序

  • 方法简单排行

排序 > 堆 > 快排

快速幂

50. Pow(x, n)

实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。

示例 1:
输入:x = 2.00000, n = 10
输出:1024.00000
示例 2:
输入:x = 2.10000, n = 3
输出:9.26100
示例 3:
输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25

提示:
-100.0 < x < 100.0
-231 <= n <= 231-1
-104 <= xn <= 104

image.png

  1. // cyc
  2. class Solution {
  3. public double myPow(double x, int n) {
  4. return n < 0 ? 1 / dfs(x, -n) : dfs(x, n);
  5. }
  6. double dfs(double x, int n) {
  7. if (n == 0) {
  8. return 1;
  9. }
  10. return (n % 2 == 0 ? 1 : x) * dfs(x * x, n / 2);
  11. }
  12. }

DFS/递归

372. 超级次方

你的任务是计算 ab 对 1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。

示例 1:
输入:a = 2, b = [3] 输出:8
示例 2:
输入:a = 2, b = [1,0] 输出:1024
示例 3:
输入:a = 1, b = [4,3,3,8,5,2] 输出:1
示例 4:
输入:a = 2147483647, b = [2,0,0] 输出:1198

提示:

  • 1 <= a <= 231 - 1
  • 1 <= b.length <= 2000
  • 0 <= b[i] <= 9
  • b 不含前导 0

image.png
image.png

  1. // cyc
  2. class Solution {
  3. final int MOD = 1337;
  4. public int superPow(int a, int[] b) {
  5. return dfs(a, b, b.length - 1);
  6. }
  7. int dfs(int a, int[] b, int index) {
  8. if (index == -1) {
  9. return 1;
  10. }
  11. return calRemain(dfs(a, b, index - 1), 10) * calRemain(a, b[index]) % MOD;
  12. }
  13. int calRemain(int base, int power) {
  14. long temp = base;
  15. while (power > 1) {
  16. temp = temp % MOD * base;
  17. power--;
  18. }
  19. return (int) (temp % 1337);
  20. }
  21. }

滑动窗口 / 双指针

☆☆☆ 689. 三个无重叠子数组的最大和

给你一个整数数组 nums 和一个整数 k ,找出三个长度为 k 、互不重叠、且 3 * k 项的和最大的子数组,并返回这三个子数组。

以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置(下标从 0 开始)。如果有多个结果,返回字典序最小的一个。

示例 1:
输入:nums = [1,2,1,2,6,7,5,1], k = 2
输出:[0,3,5]
解释:子数组 [1, 2], [2, 6], [7, 5] 对应的起始下标为 [0, 3, 5]。
也可以取 [2, 1], 但是结果 [1, 3, 5] 在字典序上更大。
示例 2:
输入:nums = [1,2,1,2,1,2,1,2,1], k = 2
输出:[0,2,4]

提示:
1 <= nums.length <= 2 * 104
1 <= nums[i] < 216
1 <= k <= floor(nums.length / 3)
通过次数18,909提交次数33,620

image.png
image.png
image.png

class Solution {
    public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int[] ans = new int[3];
        int sum1 = 0, maxSum1 = 0, maxSum1Idx = 0;
        int sum2 = 0, maxSum12 = 0, maxSum12Idx1 = 0, maxSum12Idx2 = 0;
        int sum3 = 0, maxTotal = 0;
        for (int i = k * 2; i < nums.length; ++i) {
            sum1 += nums[i - k * 2];
            sum2 += nums[i - k];
            sum3 += nums[i];
            if (i >= k * 3 - 1) {
                if (sum1 > maxSum1) {
                    maxSum1 = sum1;
                    maxSum1Idx = i - k * 3 + 1;
                }
                if (maxSum1 + sum2 > maxSum12) {
                    maxSum12 = maxSum1 + sum2;
                    maxSum12Idx1 = maxSum1Idx;
                    maxSum12Idx2 = i - k * 2 + 1;
                }
                if (maxSum12 + sum3 > maxTotal) {
                    maxTotal = maxSum12 + sum3;
                    ans[0] = maxSum12Idx1;
                    ans[1] = maxSum12Idx2;
                    ans[2] = i - k + 1;
                }
                sum1 -= nums[i - k * 3 + 1];
                sum2 -= nums[i - k * 2 + 1];
                sum3 -= nums[i - k + 1];
            }
        }
        return ans;
    }
}

3. 无重复字符的最长子串

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

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

提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成

// cyc
class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> map = new HashMap<>();    // 记录字符和其索引
        int maxLen = 0;
        int start = 0;

        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (map.containsKey(c)) {
                maxLen = Math.max(maxLen, i - start);
                int newStart = map.get(c) + 1;
                for (int j = start; j < newStart; j++){
                    map.remove(s.charAt(j));
                }
                start = newStart;
            }
            map.put(c, i);
        }
        return Math.max(maxLen, s.length() - start);
    }
}

567. 字符串的排列

给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。

示例 1:
输入:s1 = “ab” s2 = “eidbaooo”
输出:true
解释:s2 包含 s1 的排列之一 (“ba”).
示例 2:
输入:s1= “ab” s2 = “eidboaoo”
输出:false

提示:
1 <= s1.length, s2.length <= 104
s1 和 s2 仅包含小写字母

// cyc
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int len1 = s1.length();
        int len2 = s2.length();
        if (len1 > len2) {
            return false;
        }

        int[] s1Count = new int[26];
        int[] s2Count = new int[26];
        int s1Sum = 0;
        int s2Sum = 0;
        for (char c : s1.toCharArray()) {
            s1Count[c - 'a']++;
            s1Sum++;
        }

        // 左指针
        int left = -1;
        // right为右指针
        for (int right = 0; right < len2; right++) {
            char c = s2.charAt(right);
            int index = c - 'a';

            if (s1Count[index] > s2Count[index]) {
                if (s2Sum == 0) {
                    left = right;
                }
                s2Count[index]++;
                s2Sum++;
                if (s2Sum == s1Sum) {
                    return true;
                }
                // 如果s1本身不包含c
            } else if (s1Count[index] == 0) {
                if (s2Sum > 0) {
                    s2Sum = 0;
                    Arrays.fill(s2Count, 0);
                }
                // 字符c已经饱和,要剔除第一个c之前的所有字符
            } else {
                while (s2.charAt(left) != c) {
                    s2Count[s2.charAt(left++) - 'a']--;
                    s2Sum--;
                }
                left++;
            }
        }
        return false;
    }
}

713. 乘积小于K的子数组

给定一个正整数数组 nums和整数 k 。
请找出该数组内乘积小于 k 的连续的子数组的个数。

示例 1:
输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8个乘积小于100的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
示例 2:
输入: nums = [1,2,3], k = 0
输出: 0

提示:
1 <= nums.length <= 3 * 104
1 <= nums[i] <= 1000
0 <= k <= 106

image.png

  • right从0遍历到len - 1,每次遍历结束时,结果增加right - left + 1
  • right-left+1的切入点是思维要放在区间的右边往左边延伸,例如区间[1, 2, 3, 4]满足要求,固定住right(4)的点,可选区间右[4]、[4, 3]、[4, 3, 2]、[4, 3, 2, 1]即为数组的长度,也就是right-left+1。而right是递增的,此时[1, 2, 3]的区间已经处理完([3]、[3, 2]、[3、2、1])。如果从left为切入点,就会有[1, 2, 3, 4]和[1, 2, 3]都有[1],不就是重复了的错乱思维。

    // cyc
    class Solution {
      public int numSubarrayProductLessThanK(int[] nums, int k) {
          if (k <= 1) {
              return 0;
          }
    
          int len = nums.length;
          int result = 0;
          int left = 0;
          // 乘积
          int product = 1;
          for (int right = 0; right < len; right++) {
              product *= nums[right];
              // 当乘积大于k时,不断移动左指针
              while (product >= k) {
                  product /= nums[left];
                  left++;
              }
              result += right - left + 1;
          }
          return result;
      }
    }
    

序列DP

689. 三个无重叠子数组的最大和

同滑动窗口689
TODO

字符串匹配

686.重复叠加字符串匹配

给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。
注意:字符串 “abc” 重复叠加 0 次是 “”,重复叠加 1 次是 “abc”,重复叠加 2 次是 “abcabc”。

示例 1:
输入:a = “abcd”, b = “cdabcdab”
输出:3
解释:a 重复叠加三遍后为 “abcdabcdabcd”, 此时 b 是其子串。
示例 2:
输入:a = “a”, b = “aa”
输出:2
示例 3:
输入:a = “a”, b = “a”
输出:1
示例 4:
输入:a = “abc”, b = “wxyz”
输出:-1

提示:
1 <= a.length <= 104
1 <= b.length <= 104
a 和 b 由小写英文字母组成

可以想到,a重复的次数小于b.length() / a.length() + 2,那么就有极速版的解答

// cyc
class Solution {
    public int repeatedStringMatch(String a, String b) {
        int lenA = a.length();
        int lenB = b.length();
        int count = lenB / lenA;
        if (count * lenA == lenB && a.repeat(count).contains(b)) {
            return count;
        }
        if (a.repeat(count + 1).contains(b)) {
            return count + 1;
        }
        if (a.repeat(count + 2).contains(b)) {
            return count + 2;
        }
        return -1;
    }
}

速度快的是KMP算法,但是不想看暂时

双指针

1995. 统计特殊四元组

给你一个 下标从 0 开始 的整数数组 nums ,返回满足下述条件的 不同 四元组 (a, b, c, d) 的 数目 :
nums[a] + nums[b] + nums[c] == nums[d] ,且
a < b < c < d

示例 1:
输入:nums = [1,2,3,6]
输出:1
解释:满足要求的唯一一个四元组是 (0, 1, 2, 3) 因为 1 + 2 + 3 == 6 。
示例 2:
输入:nums = [3,3,6,4,5]
输出:0
解释:[3,3,6,4,5] 中不存在满足要求的四元组。
示例 3:
输入:nums = [1,1,1,3,5]
输出:4
解释:满足要求的 4 个四元组如下:
- (0, 1, 2, 3): 1 + 1 + 1 == 3
- (0, 1, 3, 4): 1 + 1 + 3 == 5
- (0, 2, 3, 4): 1 + 1 + 3 == 5
- (1, 2, 3, 4): 1 + 1 + 3 == 5

提示:
4 <= nums.length <= 50
1 <= nums[i] <= 100

方法一:暴力

// cyc
class Solution {
    public int countQuadruplets(int[] nums) {
        int len = nums.length;
        int result = 0;
        for (int i = 0; i < len - 3; i++) {
            for (int j = i + 1; j < len - 2; j++) {
                for (int k = j + 1; k < len - 1; k++) {
                    for (int p = k + 1; p < len; p++) {
                        if (nums[i] + nums[j] + nums[k] == nums[p]) {
                            result++;
                        }
                    }
                }
            }
        }
        return result;
    }
}
  • 时间复杂度O(n^4)

image.png

class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length;
        int ans = 0;
        Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();
        for (int c = n - 2; c >= 2; --c) {
            cnt.put(nums[c + 1], cnt.getOrDefault(nums[c + 1], 0) + 1);
            for (int a = 0; a < c; ++a) {
                for (int b = a + 1; b < c; ++b) {
                    ans += cnt.getOrDefault(nums[a] + nums[b] + nums[c], 0);
                }
            }
        }
        return ans;
    }
}

image.png
image.png

class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length;
        int ans = 0;
        Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();
        for (int b = n - 3; b >= 1; --b) {
            for (int d = b + 2; d < n; ++d) {
                cnt.put(nums[d] - nums[b + 1], cnt.getOrDefault(nums[d] - nums[b + 1], 0) + 1);
            }
            for (int a = 0; a < b; ++a) {
                ans += cnt.getOrDefault(nums[a] + nums[b], 0);
            }
        }
        return ans;
    }
}

image.png

贪心

55. 跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。

示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

提示:
1 <= nums.length <= 3 * 104
0 <= nums[i] <= 105

  • 刚开始用的dfs笨方法,用visited数组减少搜索dfs次数, 98ms,超过13%

    // cyc
    class Solution {
      boolean[] visited;
      int len;
    
      public boolean canJump(int[] nums) {
          len = nums.length;
          visited = new boolean[len];
          return dfs(nums, 0);
      }
    
      boolean dfs(int[] nums, int index) {
          if (index == len - 1) {
              return true;
          }
    
          int able = Math.min(index + nums[index], len - 1);
    
          for (int i = able; i >= index + 1; i--) {
              if (!visited[i] && dfs(nums, i)) {
                  return true;
              }
          }
          visited[index] = true;
          return false;
      }
    }
    
  • 贪心,2ms,96%

image.png

// cyc
class Solution {
    public boolean canJump(int[] nums) {
        int len = nums.length;
        int i = 0;
        // 最远能到达的距离
        int maxAble = 0;
        while (i <= maxAble) {
            maxAble = Math.max(maxAble, i + nums[i]);
            if (maxAble >= len - 1) {
                return true;
            }
            i++;
        }
        return false;
    }
}

45. 跳跃游戏 II

给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。

示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2

提示:
1 <= nums.length <= 104
0 <= nums[i] <= 1000

  • 先用动归写了下,2ms,超过47%

    // cyc
    class Solution {
      public int jump(int[] nums) {
          if (nums.length == 1) {
              return 0;
          }
          int len = nums.length;
          int[] dp = new int[len];
          Arrays.fill(dp, Integer.MAX_VALUE);
          dp[0] = 0;
          int nowBest = 0;
          for (int i = 0; i < len; i++) {
              int able = i + nums[i];
              if (able >= len - 1) {
                  return dp[i] + 1;
              }
              if (able > nowBest) {
                  for (int j = nowBest + 1; j <= able; j++) {
                      dp[j] = dp[i] + 1;
                  }
                  nowBest = able;
              }
          }
          return dp[len - 1];
      }
    }
    
  • 然后是贪心,1ms 99%

image.png

// cyc
class Solution {
    public int jump(int[] nums) {
        if (nums.length == 1) {
            return 0;
        }
        // 当前能到达的地方
        int able = nums[0];
        // 当前步数
        int nowStep = 1;
        int index = 1;
        while (index < nums.length - 1) {
            // 如果现在就能到,返回当前步数 + 1
            if (able >= nums.length - 1) {
                return nowStep + 1;
            }

            // 当前到不了,向前进一步,并求出进一步后能到达的地方
            nowStep++;
            int tempAble = able;
            while (index <= able) {
                tempAble = Math.max(tempAble, index + nums[index++]);
            }
            able = tempAble;
        }
        return nowStep + 1;
    }
}

旋转有序数组

剑指 Offer 11. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。

示例 1:
输入:[3,4,5,1,2]
输出:1
示例 2:
输入:[2,2,2,0,1]
输出:0

  • 刚开始写时,左侧最小值和右侧最大值固定在两端点,当midVal等于左右最值时,midVal可能在左侧有序也可能在右侧有序

    • 此时只能通过遍历的方式找到midVal在左侧还是在右侧,最坏的时间复杂度可能到O(n)
    • 不能采用r—或l++的方式,反例[3, 1, 3, 3]、[3, 3, 1, 3]

      // cyc
      class Solution {
      public int minArray(int[] numbers) {
         int len = numbers.length;
         if (len == 1) {
             return numbers[0];
         }
         int leftMin = numbers[0];
         int rightMax = numbers[len - 1];
         if (rightMax > leftMin) {
             return leftMin;
         }
      
         int l = 0;
         int r = len - 1;
      
         while (l < r) {
             int mid = l + (int) Math.ceil((r - l) / 2.0);
             int val = numbers[mid];
      
             if (numbers[mid - 1] > val) {
                 return val;
             }
      
             if (val > rightMax) {
                 l = mid + 1;
             } else if (val == rightMax && val == leftMin) {
                 /*当midVal可能在左侧也可能在右侧时,使用遍历的方式解决,略麻烦*/
      
                 // 此时可能在左侧也可能在右侧
                 int temp = mid;
                 // true 在左侧, false在右侧
                 boolean flag = true;
                 while (temp >= 0) {
                     if (numbers[temp] > val || numbers[temp] < val) {
                         flag = false;
                         break;
                     }
                     temp--;
                 }
                 if (flag) {
                     l = mid + 1;
                 } else {
                     r = mid - 1;
                 }
             } else {
                 r = mid - 1;
             }
         }
         return numbers[r];
      }
      }
      

image.png
image.png
image.png
image.png

  • 上述方法与我第一次写的方法最大的不同是,左侧最小值和右侧最大值不再固定,因为目标值是[最小值],所以目标值左右总是旋转后的有序数组,因此左右侧最值是可以随着二分搜索的left和right指针移动的
  • 当左右最值就是left和right指针所指的值时,当midVal等于左右最值时,将左轴或右轴移动一下就可以了

    // cyc
    class Solution {
      public int minArray(int[] numbers) {
          int len = numbers.length;
          if (len == 1) {
              return numbers[0];
          }
          int l = 0;
          int r = len - 1;
          int leftMin = numbers[l];
          int rightMax = numbers[r];
    
          // 当numbers整体有序时,直接返回最小值
          if (rightMax > leftMin) {
              return leftMin;
          }
    
          while (l < r) {
              int mid = l + (r - l) / 2;
              int val = numbers[mid];
    
              if (val > rightMax) {
                  l = mid + 1;
              } else if (val == rightMax && val == leftMin) {
                  // l++也可以
                  r--;
              } else {
                  r = mid;
              }
    
              // 更新最值
              leftMin = numbers[l];
              rightMax = numbers[r];
          }
          return numbers[l];
      }
    }
    

中序遍历

剑指 Offer 36. 二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

为了让您更好地理解问题,以下面的二叉搜索树为例:
image.png
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
image.png
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

  • 自写解

    class Solution {
      Node minNode;
      Node maxNode;
    
      public Node treeToDoublyList(Node root) {
          if (root == null) {
              return null;
          }
          minNode = root;
          maxNode = root;
          traverse(root, true, null, null);
          minNode.left = maxNode;
          maxNode.right = minNode;
          return minNode;
      }
    
      void traverse(Node node, boolean leftChild, Node leftFather, Node rightFather) {
          if (node != null) {
              Node tempRight = node.right;
              traverse(node.left, true, node, rightFather);
              traverse(tempRight, false, leftFather, node);
    
              if (leftChild) {
                  if (node.right == null) {
                      node.right = leftFather;
                  }
                  if (node.left == null) {
                      if (rightFather != null && rightFather.val < node.val) {
                          node.left = rightFather;
                          rightFather.right = node;
                      } else {
                          minNode = node.val < minNode.val ? node : minNode;
                      }
                  }
              } else {
                  if (node.left == null) {
                      node.left = rightFather;
                  }
                  if (node.right == null) {
                      if (leftFather != null && leftFather.val > node.val) {
                          node.right = leftFather;
                          leftFather.left = node;
                      } else {
                          maxNode = node.val > maxNode.val ? node : maxNode;
                      }
                  }
              }
          }
      }
    }
    

image.png

class Solution {
    Node pre, head;
    public Node treeToDoublyList(Node root) {
        if(root == null) return null;
        dfs(root);
        head.left = pre;
        pre.right = head;
        return head;
    }
    void dfs(Node cur) {
        if(cur == null) return;
        dfs(cur.left);
        if(pre != null) pre.right = cur;
        else head = cur;
        cur.left = pre;
        pre = cur;
        dfs(cur.right);
    }
}

排序

剑指 Offer 45. 把数组排成最小的数

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:
输入: [10,2]
输出: “102”
示例 2:
输入: [3,30,34,5,9]
输出: “3033459”

提示:
0 < nums.length <= 100

说明:
输出结果可能非常大,所以你需要返回一个字符串而不是整数
拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0

// cyc
class Solution {
    public String minNumber(int[] nums) {
        List<String> list = Arrays.stream(nums).mapToObj(String::valueOf).collect(Collectors.toList());
        list.sort((o1, o2) -> (o1 + o2).compareTo(o2 + o1));
        StringBuilder sb = new StringBuilder();
        for (String num : list) {
            sb.append(num);
        }
        return sb.toString();
    }
}

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

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

例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。

示例 1:
输入:
[“MedianFinder”,”addNum”,”addNum”,”findMedian”,”addNum”,”findMedian”]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:
输入:
[“MedianFinder”,”addNum”,”findMedian”,”addNum”,”findMedian”]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

限制:
最多会对 addNum、findMedian 进行 50000 次调用。

  • 使用一个大根堆一个小根堆即可

    // cyc
    class MedianFinder {
      private PriorityQueue<Integer> smallHeap;
      private PriorityQueue<Integer> bigHeap;
      private int total;
    
      public MedianFinder() {
          total = 0;
          smallHeap = new PriorityQueue<>();
          bigHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
      }
    
      public void addNum(int num) {
          if (bigHeap.isEmpty()) {
              bigHeap.add(num);
          } else {
              int big = bigHeap.peek();
              if (num < big) {
                  if (bigHeap.size() > smallHeap.size()) {
                      smallHeap.add(bigHeap.poll());
                  }
                  bigHeap.add(num);
              } else {
                  smallHeap.add(num);
                  if (bigHeap.size() < smallHeap.size()) {
                      bigHeap.add(smallHeap.poll());
                  }
              }
          }
          total++;
      }
    
      public double findMedian() {
          if (total % 2 == 0) {
              return (bigHeap.peek() + smallHeap.peek()) / 2.0;
          } else {
              return bigHeap.peek();
          }
      }
    }
    

动态规划

918. 环形子数组的最大和

给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 。
环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。
子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], …, nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。

示例 1:
输入:nums = [1,-2,3,-2]
输出:3
解释:从子数组 [3] 得到最大和 3
示例 2:
输入:nums = [5,-3,5]
输出:10
解释:从子数组 [5,5] 得到最大和 5 + 5 = 10
示例 3:
输入:nums = [3,-2,2,-3]
输出:3
解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3

提示:
n == nums.length
1 <= n <= 3 104
-3
104 <= nums[i] <= 3 * 104

// cyc
class Solution {
    public int maxSubarraySumCircular(int[] nums) {
        int sum = nums[0];
        int pre = nums[0];
        int min = nums[0];
        for (int i = 1; i < nums.length; i++) {
            sum += nums[i];
            pre = Math.min(nums[i], pre + nums[i]);
            min = Math.min(min, pre);
        }

        pre = nums[0];
        int max = nums[0];
        for (int i = 1; i < nums.length; i++) {
            pre = Math.max(nums[i], pre + nums[i]);
            max = Math.max(max, pre);
        }
        return max < 0 ? max : Math.max(max, sum - min);
    }
}

洗牌算法

共有 n 个不同的数,根据每个位置能够选择什么数,共有 n! 种组合。

题目要求每次调用 shuffle 时等概率返回某个方案,或者说每个元素都够等概率出现在每个位置中。
我们可以使用 Knuth 洗牌算法,在 O(n) 复杂度内等概率返回某个方案。
具体的,我们从前往后尝试填充 [0, n−1] 该填入什么数时,通过随机当前下标与(剩余的)哪个下标进行值交换来实现。

对于下标 x 而言,我们从 [x,n−1] 中随机出一个位置与 xx 进行值交换,当所有位置都进行这样的处理后,我们便得到了一个公平的洗牌方案。

对于下标为 0 位置,从 [0, n−1] 随机一个位置进行交换,共有 n 种选择;下标为 1 的位置,从 [1, n - 1] 随机一个位置进行交换,共有 n - 1 种选择 …

代码:

class Solution {
    int[] nums;
    int n;
    Random random = new Random();
    public Solution(int[] _nums) {
        nums = _nums;
        n = nums.length;
    }
    public int[] reset() {
        return nums;
    }
    public int[] shuffle() {
        int[] ans = nums.clone();
        for (int i = 0; i < n; i++) {
            swap(ans, i, i + random.nextInt(n - i));
        }
        return ans;
    }
    void swap(int[] arr, int i, int j) {
        int c = arr[i];
        arr[i] = arr[j];
        arr[j] = c;
    }
}

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