- 0003.无重复字符的最长子串.md
- 0005.最长回文子串.md
- 0007.K个一组反转链表.md
- 0009. 数组中的第K个最大元素.md
- 0015.三数之和.md
- 0031.下一个排列.md
- 0032. 最长有效括号.md
- 0033. 搜索旋转排序数组.md
- 0041.缺失的第一个正整数.md
- 0042. 接雨水.md
- 0046. 全排列.md
- 0053. 最大子序和.md
- 0054. 螺旋矩阵.md
- 0056.合并区间.md
- 0069.x的平方根.md
- 0076.最小覆盖子串.md
- 0093. 复原 IP 地址.md
- 0098.验证是否为二叉搜索树.md
- 0101.对称二叉树.md
- 0104. 二叉树的最大深度.md
- 0105. 从前序与中序遍历序列构造二叉树.md
- 0106. 从中序与后序遍历序列构造二叉树.md
- 0110. 平衡二叉树.md
- 0111. 二叉树的最小深度.md
- 0112.路径总和.md
- 0121. 买卖股票的最佳时机.md
- 0122. 买卖股票的最佳时机 II.md
- 0124. 二叉树中的最大路径和.md
- 0129. 求根节点到叶节点数字之和.md
- 0141. 环形链表.md
- 0143. 重排链表.md
- 0146.手撕LRU.md
- 0148.链表排序.md
- 0155. 最小栈.md
- 0160. 相交链表.md
- 0162. 寻找峰值.md
- 0199.二叉树的右视图.md
- 0200.岛屿数量.md
- 0206.反转链表.md
- 0208. 实现 Trie (前缀树).md
- 0221. 最大正方形.md
- 0232. 使用栈实现队列.md
- 使用两个栈实现先入先出队列
- 0236. 二叉树的最近公共祖先.md
- 0260. 只出现一次的数字 III.md
- 0300.最长递增序列.md
- 0328. 奇偶链表.md
- 0334.递增的三元子序列.md
- 0415.字符串相加.md
- 0704.二分查找.md
- 0912. 快速排序.md
- 20. 有效的括号.md
- 234. 回文链表.md
- 460. LFU 缓存.md
- 470. 用 Rand7() 实现 Rand10().md
- 8. 字符串转换整数 (atoi).md
- 剑指 Offer 15. 二进制中1的个数.md
- 剑指 Offer 41. 数据流中的中位数.md
- 剑指 Offer 50. 第一个只出现一次的字符.md
- 剑指 Offer 51. 数组中的逆序对.md
- 剑指 Offer 59 - I. 滑动窗口的最大值.md
- 剑指 Offer 66. 构建乘积数组.md
- 剑指 Offer II 001. 整数除法.md
- 剑指 Offer II 002. 二进制加法.md
- 剑指 Offer II 003. 前 n 个数字二进制中 1 的个数.md
- 剑指 Offer II 004. 只出现一次的数字 .md
- 剑指 Offer II 005. 单词长度的最大乘积.md
- 剑指 Offer II 006. 排序数组中两个数字之和.md
- 剑指 Offer II 008. 和大于等于 target 的最短子数组.md
- 多线程交替打印.md
- 奇升偶降链表.md
- 字节跳动高频题——排序奇升偶降链表
- 手撕单例模式.md
- 手撕归并排序.md
- 手撕线程池.md
0003.无重复字符的最长子串.md
关键字 : 字符串 重复 最长子串 滑动窗口
给定一个字符串
s
,请你找出其中不含有重复字符的 最长子串 的长度。输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,”pwke” 是一个子序列,不是子串。
public int lengthOfLongestSubstring(String s) {
if (s.length() <= 1){
return s.length();
}
// 子串起点
int startIndex = 0;
// 记录最长子串长度
int maxLen = 1;
// 窗口右边界不断右移
for (int endIndex = 1; endIndex < s.length(); endIndex++) {
int tempLen = 1;
// 每次右边界右移一位就需要判断窗口中是否存在当前右边界字符
// 如果窗口中存在右边界字符, 则窗口左移动到窗口中重复元素的后一个位置
for (int i = startIndex; i < endIndex ; i++) {
if (s.charAt(i) == s.charAt(endIndex)){
startIndex = i+1;
break;
}
}
// 更新答案
maxLen = Math.max(maxLen, endIndex-startIndex+1);
}
return maxLen;
}
0005.最长回文子串.md
关键字 : 字符串 最长回文串 动归
给你一个字符串
s
,找到s
中最长的回文子串。
public String longestPalindrome(String s) {
// 特殊用例判断
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
int start = 0;
// dp[i][j] 表示 s[i, j] 是否是回文串
boolean[][] dp = new boolean[len][len];
char[] charArray = s.toCharArray();
// 单个的字符肯定是回文串
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
// 计算dp[i][j]
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
if (charArray[i] != charArray[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}
}
}
return s.substring(start, start + maxLen);
}
0007.K个一组反转链表.md
关键字 : 链表 反转
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
// 使用一个固定长度的栈
public static ListNode reverseKGroup(ListNode head, int k) {
ListNode p = head;
// 栈的大小为k
ArrayDeque<ListNode> stack = new ArrayDeque<>(k);
ListNode pt = new ListNode(-1);
ListNode ans = pt;
// p指针往后走的过程中, 将节点入栈
while (p != null){
stack.addFirst(p);
p = p.next;
// 栈中有K个节点了
if (stack.size() == k){
// 对栈中节点进行反转
// pt指向的是当前反转的最后一个节点
while (!stack.isEmpty()){
pt.next = stack.removeFirst();
pt = pt.next;
}
}
}
// 最后栈中还有元素, 处理剩下的不满k个的节点
// 此处处理方式是, 保留原链表顺序, 即将stack以队列方式出队
while (!stack.isEmpty()){
pt.next = stack.removeLast();
pt = pt.next;
}
pt.next = null;
return ans.next;
}
0009. 数组中的第K个最大元素.md
关键字 : 数组 第k大 快排 堆排序 优先队列
给定整数数组
nums
和整数k
,请返回数组中第**k**
个最大的元素。请注意,你需要找的是数组排序后的第
k
个最大的元素,而不是第k
个不同的元素。示例:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
方法一
// 使用优先队列(默认底层实现为最小堆)
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int i = 0; i < k; i++) {
pq.add(nums[i]);
}
for (int i = k; i < nums.length; i++) {
if (!pq.isEmpty() && nums[i] > pq.peek()){
pq.remove();
pq.add(nums[i]);
}
}
return pq.peek();
}
方法二
// 利用快排过程
private int k;
private int ans;
public int findKthLargest1(int[] nums, int k) {
this.k = k;
sort(nums, 0, nums.length-1);
return ans;
}
public void sort(int[] arr, int left, int right){
if (left >= right) return;
int p = partition(arr, left, right);
// 第k大的对应第arr.length - k小的(从0开始算)
if (p == arr.length - k){
ans = arr[p];
return;
}
// 左边排序
sort(arr, left, p-1);
// 右边排序
sort(arr, p+1, right);
}
private int partition(int[] arr, int left, int right) {
// 优化点 : 随机选取枢轴元素. 将选中的枢轴元素交换到最左边
int p = left + (new Random()).nextInt(right-left+1);
swap(arr, left, p);
// 双指针移动
int i = left+1, j = right;
while (true){
// 从左往右找到第一个比枢纽元素大的, 注意循环条件
while (i <= j && arr[i] < arr[left])
i++;
// 从右往左找到第一个比枢纽元素小的,注意循环条件
while (i <= j && arr[j] > arr[left])
j--;
// 循环终止条件
if (i >= j) break;
// 交换两个位置的元素
swap(arr, i, j);
// i往后走一步, j往前走一步, 继续循环
i++;
j--;
}
// 最后交换枢纽元素和j停留的位置
swap(arr, left, j);
return j;
}
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
0015.三数之和.md
关键字 : 数组 三数之和 排序 双指针
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复答案。
思路 : 排序+双指针
时间复杂度 O(n^2)
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
// 先排序
Arrays.sort(nums);
for (int i = 0; i < nums.length - 1; i++) {
// 如果首元素大于0, 则后面不可能找出两个数的和=-首元素
if(nums[i] > 0) return ans;
// 跳过重复解
if (i>0 && nums[i] == nums[i-1]){
continue;
}
int l = i+1;
int r = nums.length-1;
while (l<r){
if (nums[l] + nums[r] + nums[i] < 0){
++l;
}else if (nums[l] + nums[r] + nums[i] > 0){
--r;
}else {
ArrayList<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[l]);
list.add(nums[r]);
ans.add(list);
// 跳过重复解
while (l<r && nums[l] == nums[l+1]){
++l;
}
++l;
while (l<r && nums[r] == nums[r-1]){
--r;
}
--r;
}
}
}
return ans;
}
附赠两数之和
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(nums[i])){
return new int[]{map.get(nums[i]),i};
}
map.put(target - nums[i], i);
}
return null;
}
0031.下一个排列.md
关键字 下一个 字典序
实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须 原地 修改,只允许使用额外常数空间。
示例
输入:nums = [1,2,3]
输出:[1,3,2]
输入:nums = [3,2,1]
输出:[1,2,3]
class Solution{
public static void nextPermutation(int[] nums) {
int n = nums.length;
// 从后往前找第一个变小的数字
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i+1]){
i--;
}
// 除了while循环,有两种情况,一是i==-1了, 二是找到了num[i] < num[i+1]
if (i >= 0){
// 从后往前找第一个比num[i]大的数,与num[i]交换
for (int j = n-1; j > i; j--){
if (nums[j] > nums[i]){
// 找到了
swap(nums, i, j);
break;
}
}
}
// 交换完后,需要把后面一部分整体反转,使后面一部分是最小的
reverse(nums, i+1, n-1);
}
private static void swap(int[] nums, int i, int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
// 反转数组区间[lo, hi]
private static void reverse(int[] nums, int lo, int hi){
while (lo < hi){
swap(nums, lo++, hi--);
}
}
}
0032. 最长有效括号.md
给你一个只包含
'('
和')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。思路 : 动态规划
我们定义 dp[_i] 表示以下标 _i* 字符结尾的最长有效括号的长度。因此我们知道以 ‘(’ 结尾的子串对应的 dp 值必定为 0 ,我们只需要求解 ‘)’ 在 dp 数组中对应位置的值。
我们从前往后遍历字符串求解 dp 值:
当s[i] = ‘)’ 的时候, 有两种情况 :
- s[i - 1] =‘(’ : 这种情况我们可以推出 : dp[i]=dp[i−2]+2
- s[i - 1] =‘)’ : 这种情况时又分两种情况
- s[ i - dp[i-1] - 1 ] = ‘(‘ : 这种情况我们可以推出 : dp[i] = dp[i−1] +2 + dp[ i− dp[i−1] − 2 ]
- s[ i - dp[i-1] - 1 ] = ‘)’ : dp[i] = 0
public static int longestValidParentheses(String s) {
int[] dp = new int[s.length()];
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '('){
dp[i] = 0;
}else { // ')'
if (i > 0 && s.charAt(i-1) == '('){
if (i-2>=0)
dp[i] = dp[i-2] + 2;
else
dp[i] = 2;
}else if (i > 0 && s.charAt(i-1) == ')'){ // ....))
// 判断 i-dp[i-1]-1 是否是'('
if (i-dp[i-1]-1 >= 0 && s.charAt(i-dp[i-1]-1) == '('){
if (i-dp[i-1]-2 >= 0)
dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2];
else
dp[i] = dp[i-1] + 2;
}else {
dp[i] = 0;
}
}
}
}
int ans = 0;
for (int i = 0; i < s.length(); i++) {
ans = Math.max(ans, dp[i]);
}
return ans;
}
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
。
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = (right - left) / 2 + left;
// 找到直接返回
if (nums[mid] == target) {
return mid;
}
// 如果nums[mid] >= nums[left], 说明mid在旋转点的左边
if (nums[mid] >= nums[left]) {
if (nums[mid] > target && target >= nums[left]) { // 去左边找
right = mid - 1;
} else { // 去右边找
left = mid + 1;
}
} else { // 说明mid在旋转点的右边
if (nums[mid] < target && target <= nums[right]) { // 去右边找
left = mid + 1;
} else { // 去左边找
right = mid - 1;
}
}
}
return -1;
}
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 的地方去
public int firstMissingPositive(int[] nums) {
int len = nums.length;
// 循环一遍, 交换数字到应该在的位置
for (int i = 0; i < len; i++) {
while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
// 满足在指定范围内、并且没有放在正确的位置上,才交换
// 例如:数值 3 应该放在索引 2 的位置上
swap(nums, nums[i] - 1, i);
}
}
// 遍历一遍, 找到第一个位置不符合的元素返回
for (int i = 0; i < len; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 上面for循环没有返回, 则说明缺失的是最大的那个数 即数组长度 + 1
return len + 1;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[index2] = temp;
}
0042. 接雨水.md
给定 n 个非负整数表示每个宽度为 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 个单位的雨水(蓝色部分表示雨水)。思路 : 对于每一列上积攒的雨水量的计算方法是 : 找出当前列左边的最高处, 和右边的最高处, 取二者较小者就是水高线
因此, 方法一就是暴力求解, 对于每一个格子(列), 分别找它左边右边的最高点, 然后减去它自身的高度, 就是当前格子里的积水量, 然后累加在一起即可.
方法一 : 暴力解法
public int trap1(int[] height) {
int ans = 0;
for (int i = 0; i < height.length-1; i++) {
int leftMaxHi = findMax(height, 0, i);
int rightMaxHi = findMax(height, i+1, height.length-1);
int cur = Math.min(leftMaxHi, rightMaxHi) - height[i];
if (cur>0) ans += cur;
}
return ans;
}
// 在[left, right]区间找最大值
private int findMax(int[] arr, int left, int right) {
int ret = arr[left];
while (left <= right){
ret = Math.max(ret, arr[left]);
left++;
}
return ret;
}
方法二 : 动归
思路与方法一一样, 只是用了两个辅助数组分别记录num[i]左右最高点
public int trap2(int[] height) {
int n = height.length; if (n <= 1) return 0;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
// 记录num[i]左边最大值(包含i)
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i-1], height[i]);
}
// 记录num[i]右边最大值(包含i)
rightMax[n -1] = height[n -1];
for (int i = n -2; i >= 0; i--){
rightMax[i] = Math.max(rightMax[i+1], height[i]);
}
// 求结果
int ans = 0;
for (int i = 0; i < n - 1; i++) {
ans += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
0046. 全排列.md
关键字 : 数组 字符串 全排列 回溯 深度优先
给定一个不含重复数字的数组
nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。注 : 如果给定一个字符串, 求字符全排列, 则将字符串转成char数组
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
boolean[] used = new boolean[len];
List<List<Integer>> ans = new ArrayList<>();
dfs(nums, used, len, 0, ans, new ArrayList<>());
return ans;
}
private void dfs(int[] nums, boolean[] used, int len, int depth, List<List<Integer>> ans, List<Integer> list){
if (len == depth){
ans.add(new ArrayList<>(list));
return;
}
for (int i = 0; i < len; i++) {
if (!used[i]){
list.add(nums[i]);
used[i] = true;
dfs(nums, used, len, depth+1, ans, list);
used[i] = false;
list.remove(Integer.valueOf(nums[i]));
}
}
}
0053. 最大子序和.md
关键字 : 数组 最大子序列和 连续子序列和 动归
给定一个整数数组
nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
public int maxSubArray(int[] nums) {
int maxSum = nums[0];
// 记录以num[i]结尾的最大子序列和
for (int i = 1; i < nums.length; i++) {
// 如果以nums[i-1]结尾的最大子序列和>0, 则以nums[i]结尾的最大子序列和累加上上一个, 否则就是它本身
if (nums[i-1] > 0){
nums[i] += nums[i-1];
}
// 更新答案
if (nums[i] > maxSum){
maxSum = nums[i];
}
}
return maxSum;
}
或者 :
f(i)=max{f(i−1)+nums[i],nums[i]}
public int maxSubArray(int[] nums) {
int pre = 0, maxAns = nums[0];
for (int x : nums) {
pre = Math.max(pre + x, x);
maxAns = Math.max(maxAns, pre);
}
return maxAns;
}
0054. 螺旋矩阵.md
关键字 : 螺旋矩阵 顺时针打印 二维数组 模拟
给你一个
m
行n
列的矩阵matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。思路 : 一层一层从外到里打印, 使用四个变量 r1, r2, c1, c2 分别存储上下左右边界值,从而定义当前最外层。
注意只有在 r1 != r2 时才打印最下一行,也就是在当前最外层的行数大于 1 时才打印最下一行
public List<Integer> printMatrix(int[][] matrix) {
List<Integer> ans = new ArrayList<>();
int r1 = 0, c1 = 0;
int r2 = matrix.length - 1, c2 = matrix[0].length - 1;
while (r1 <= r2 && c1 <= c2) {
// 上
for (int i = c1; i <= c2; i++)
ans.add(matrix[r1][i]);
// 右
for (int i = r1 + 1; i <= r2; i++)
ans.add(matrix[i][c2]);
if (r1 != r2)
// 下
for (int i = c2 - 1; i >= c1; i--)
ans.add(matrix[r2][i]);
if (c1 != c2)
// 左
for (int i = r2 - 1; i > r1; i--)
ans.add(matrix[i][c1]);
r1++; r2--; c1++; c2--;
}
return ans;
}
0056.合并区间.md
关键词 : 数组 合并 区间合并 排序
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
public int[][] merge(int[][] intervals) {
if (intervals.length == 0) return new int[0][2];
// 按照区间第一位排序
Arrays.sort(intervals, (interval1, interval2) -> interval1[0] - interval2[0]);
List<int[]> ans = new ArrayList<int[]>();
for (int[] interval : intervals) {
int L = interval[0], R = interval[1];
// 列表为空或者列表中最后一个区间右边界小于当前区间左边界,则表示不能与列表中的区间合并,新添加一个区间
if (ans.size() == 0 || ans.get(ans.size() - 1)[1] < L) {
ans.add(new int[]{L, R});
} else { // 否则当前区间可以与列表中最后一个区间合并
ans.get(ans.size() - 1)[1] = Math.max(ans.get(ans.size() - 1)[1], R);
}
}
return ans.toArray(new int[ans.size()][]);
}
0069.x的平方根.md
关键词 : 开方 平方根 二分
实现 sqrt(int x)
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
0076.最小覆盖子串.md
关键词 : 字符串 子串 最短覆盖子串 滑动窗口
给你一个字符串
s
、一个字符串t
。返回s
中涵盖t
所有字符的最小子串。如果s
中不存在涵盖t
所有字符的子串,则返回空字符串""
。
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
public String minWindow(String s, String t) {
if (s.length() == 0 || t.length() == 0 || s.length() < t.length()){
return "";
}
int[] need = new int[128];
//记录需要的字符的个数
for (int i = 0; i < t.length(); i++) {
need[t.charAt(i)]++;
}
//l是当前左边界,r是当前右边界,size记录窗口大小,count是需求的字符个数,start是最小覆盖串开始的index
int left = 0, right = 0, windowSize = s.length()+1, needCharCount = t.length(), start = 0;
//遍历所有字符
while (right < s.length()) {
char c = s.charAt(right);
if (need[c] > 0) { //需要字符c
needCharCount--;
}
// 进入窗口的所有字符都加入need数组,如果need[index]<0 , 说明是不需要的冗余字符
need[c]--;//把右边的字符加入窗口
if (needCharCount == 0) {//窗口中已经包含所有字符
while (left < right && need[s.charAt(left)] < 0) {
// 只要遇到冗余字符,则将该字符从窗口中移除,并且left++
need[s.charAt(left)]++;//释放右边移动出窗口的字符
left++;//指针右移
}
// 出了while循环表示窗口无法再缩小了,此时更新答案
if (right - left + 1 < windowSize) {//不能右移时候挑战最小窗口大小,更新最小窗口开始的start
windowSize = right - left + 1;
start = left;//记录下最小值时候的开始位置,最后返回覆盖串时候会用到
}
//更新完答案后,将left继续向左移动一位,使得窗口不满足要求,需要继续right右移寻找新的答案
need[s.charAt(left)]++;
left++;
needCharCount++;
}
right++;
}
return windowSize == s.length()+1 ? "" : s.substring(start, start + windowSize);
}
0093. 复原 IP 地址.md
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
输入:s = "0000"
输出:["0.0.0.0"]
// 回溯
private List<String> ans = new ArrayList<>();
public List<String> isIp(String s) {
int n = s.length();
if (n > 12 || n < 4) return ans;
// 队列
Deque<String> path = new ArrayDeque<>(4);
dfs(s, 0, 4, path);
return ans;
}
private void dfs(String s, int start, int segment, Deque<String> path) {
int n = s.length();
if (start == n && segment == 0) {
StringBuilder sb = new StringBuilder();
for (String str : path) {
sb.append(str).append(".");
}
sb.deleteCharAt(sb.length()-1);
ans.add(sb.toString());
return ;
}
// 最多3位组成一个段, i位当前段的尾部下标
for (int i=start;i<start+3;i++) {
if (i >= n) break;
// 判断剩下的字符串长度是否比要组成的段的最大长度还长,如果是,则当前答案无效,继续下一次循环
if (segment * 3 < n - i) continue;
if (isIpSegment(s, start, i)) {
// 当前段有效,加入队列
String currentSegment = s.substring(start, i + 1);
path.addLast(currentSegment);
// 关键, 下一段从下标为i+1的位置开始,总段数-1
dfs(s, i + 1, segment - 1, path);
// 回溯
path.removeLast();
}
}
}
private boolean isIpSegment(String s, int left, int right) {
// 如果字符串长度大于1, 且第一位是0,则不可为ip段
if (right - left + 1 > 1 && s.charAt(left) == '0') return false;
int res = Integer.parseInt(s.substring(left, right+1));
return res >= 0 && res <= 255;
}
0098.验证是否为二叉搜索树.md
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
中序遍历即可
private int preNum = Integer.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
boolean left = isValidBST(root.left);
if (!left){
return false;
}
if (root.val <= preNum){ // 不满足中序有序规律
return false;
}
preNum = root.val;
return isValidBST(root.right);
}
0101.对称二叉树.md
关键词 : 二叉树 对称
给定一个二叉树,检查它是否是镜像对称的。
如
1
/ \
2 2
/ \ / \
3 4 4 3
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
return isSymmetric(root.left, root.right);
}
public boolean isSymmetric(TreeNode node1, TreeNode node2) {
if (node1 == null && node2 == null) return true;
if (node1 == null || node2 == null) return false;
// 先序判断值相等
if (node1.val == node2.val){
// 然后判断node1的左孩子是否和node2的右孩子对称
boolean f1 = isSymmetric(node1.left, node2.right);
// 然后判断node1的右孩子是否和node2的左孩子对称
boolean f2 = isSymmetric(node1.right, node2.left);
return f1 && f2;
}else
return false;
}
0104. 二叉树的最大深度.md
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
思路 : DFS, 后序递归
public int maxDepth(TreeNode root) {
if (root == null) return 0;
if (root.left == null && root.right == null )
return 1;
else if (root.left == null){
// 左树为空, 则返回右树高度+1
return maxDepth(root.right)+1;
}else if (root.right == null){
// 右树为空, 则返回左树高度+1
return maxDepth(root.left)+1;
}else {
// 去左右子树高度最大值+1
return Math.max(maxDepth(root.right)+1, maxDepth(root.left)+1);
}
}
0105. 从前序与中序遍历序列构造二叉树.md
关键词 : 二叉树 重建 前序中序
给定一棵树的前序遍历
preorder
与中序遍历inorder
。请构造二叉树并返回其根节点。
// 缓存中序遍历数组每个值对应的索引
private Map<Integer, Integer> indexForInOrders = new HashMap<>();
public TreeNode buildTree(int[] pre, int[] in) {
// 存中序节点索引
for (int i = 0; i < in.length; i++)
indexForInOrders.put(in[i], i);
return buildTree(pre, 0, pre.length - 1, 0);
}
// 只需要前序,前序中当前子树的左右范围,中序左右范围
private TreeNode buildTree(int[] pre, int preL, int preR, int inL) {
if (preL > preR)
return null;
// 当前子树的根
TreeNode root = new TreeNode(pre[preL]);
// 从中序中找出根的位置
int inIndex = indexForInOrders.get(root.val);
// 当前子树中左子树的节点数量
int leftTreeSize = inIndex - inL;
// 递归
root.left = buildTree(pre, preL + 1, preL + leftTreeSize, inL);
root.right = buildTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1);
return root;
}
0106. 从中序与后序遍历序列构造二叉树.md
关键词 : 二叉树 重建 后序中序
根据一棵树的中序遍历与后序遍历构造二叉树。
// 缓存中序遍历数组每个值对应的索引
HashMap<Integer,Integer> indexForInOrders = new HashMap<>();
public TreeNode buildTree(int[] inorder, int[] postorder) {
// 存中序节点索引
for(int i = 0;i < inorder.length; i++)
indexForInOrders.put(inorder[i], i);
return buildTree(postorder ,0, 0, postorder.length - 1);
}
public TreeNode buildTree(int[] postorder, int inL, int postL, int postR) {
if(postR < postL) return null;
// 当前子树的根
TreeNode root = new TreeNode(postorder[postR]);
// 从中序中找出根的位置
int inIndex = indexForInOrders.get(root.val);
root.left = buildTree(postorder ,inL, postL, postL + inIndex - inL - 1);
root.right = buildTree(postorder ,inIndex + 1, postL + inIndex - inL, postR - 1);
return root;
}
0110. 平衡二叉树.md
给定一个二叉树,判断它是否是高度平衡的二叉树。
思路 : 后序递归
public boolean isBalanced(TreeNode root) {
int i = treeHeight(root);
return i != -1;
}
private int treeHeight(TreeNode node){
if (node == null) return 0;
// 计算左子树的高度
int leftHeight = treeHeight(node.left);
// 如果左子树高度为-1, 说明左子树不平衡
if (leftHeight == -1) return -1;
// 计算右子树的高度
int rightHeight = treeHeight(node.right);
// 如果右子树高度为-1, 说明右子树不平衡
if (rightHeight == -1) return -1;
// 如果左子树右子树的高度差不超过1, 则返回最大高度, 超过1则返回-1表示不平衡
return Math.abs(leftHeight-rightHeight)<=1 ? Math.max(leftHeight, rightHeight)+1 : -1;
}
0111. 二叉树的最小深度.md
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
// BFS
public int minDepth(TreeNode root) {
if (root == null) return 0;
Deque<TreeNode> queue = new ArrayDeque<>();
queue.addLast(root);
// 初试深度设为1
int depth = 1;
while (!queue.isEmpty()){
// 当前所在层有多少个节点
int curLevelNodeNum = queue.size();;
for (int i = 0; i < curLevelNodeNum; i++) { // 将一层节点全部出完之后, 深度+1
TreeNode node = queue.removeFirst();
if (node.left == null && node.right == null) return depth;
if (node.left != null) queue.addLast(node.left);
if (node.right != null) queue.addLast(node.right);
}
// 出完一层, 深度++
depth++;
}
return depth;
}
0112.路径总和.md
关键字 : 二叉树 路径总和 递归
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) {
return false;
}
if (root.left == null && root.right == null) {
return sum == root.val;
}
return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}
扩展, 找出所有路径总和=targetSum 的路径
List<List<Integer>> ans = new ArrayList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
dfs(root, targetSum);
return ans;
}
public void dfs(TreeNode root, int targetSum) {
if (root == null) {
return;
}
path.addLast(root.val);
targetSum -= root.val; // targetSum - 当前节点值
// 到达叶子结点, 且targetSum减到0, 添加一个答案
if (root.left == null && root.right == null && targetSum == 0) {
ans.add(new ArrayList<Integer>(path));
}
dfs(root.left, targetSum);
dfs(root.right, targetSum);
//
path.removeLast();
}
0121. 买卖股票的最佳时机.md
关键字 : 股票 最佳时机 买卖一次 最大收益 最大利润 动归
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
思路 : 使用一个变量记录 prices[i] 之前的最低价 minPrice, 每次使用 prices[i] - minPrice, 如果收益更多, 则更新收益
public int maxProfit(int[] prices) {
int ans = 0;
int minPrice = prices[0];
for(int i=1;i <prices.length;i++){
ans = Math.max(ans, prices[i]-minPrice);
minPrice = Math.min(minPrice, prices[i]);
}
return ans;
}
0122. 买卖股票的最佳时机 II.md
关键字 : 股票 最佳时机 买卖多次 最大收益 最大利润 贪心
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
public int maxProfit(int[] prices) {
int ans = 0;
int curMin = prices[0];
for (int i = 1; i < prices.length; i++) {
curMin = Math.min(curMin, prices[i]);
// 只要赚钱就卖掉
if (prices[i] > curMin){
// 卖掉
ans += prices[i] - curMin;
curMin = prices[i];
}
}
return ans;
}
0124. 二叉树中的最大路径和.md
关键词 : 二叉树 最大路径和 关键路径 后序递归
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42思路, 后序递归, 将dfs(TreeNode node)定义为以node为根的二叉树且路径必须经过node节点的路径最大和
private int maxAns = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return maxAns;
}
// 后序递归
private int dfs(TreeNode node) {
if (node == null) return 0;
int left = Math.max(0, dfs(node.left));
int right = Math.max(0, dfs(node.right));
// 三种情况
// 1. 路径为左边+当前节点
// 2. 路径为右边+当前节点
// 3. 路径为左边+当前节点+右边
int sum = node.val + left + right;
// 更新答案
maxAns = Math.max(maxAns, sum);
// 返回上一层的结果只能是选择当前节点左右两边的其中一边
return node.val + Math.max(left, right);
}
0129. 求根节点到叶节点数字之和.md
给你一个二叉树的根节点
root
,树中每个节点都存放有一个0
到9
之间的数字。每条从根节点到叶节点的路径都代表一个数字:
输入:root = [4,9,0,5,1]
输出:495 + 491 + 40 = 1026
public int sumNumbers(TreeNode root) {
return dfs(root, 0);
}
public int dfs(TreeNode node, int prefix) {
if (root == null) {
return 0;
}
int newPrefix = prefix * 10 + node.val;
if (node.left == null && node.right == null) {
return newPrefix;
} else {
return dfs(node.left, newPrefix) + dfs(node.right, newPrefix);
}
}
0141. 环形链表.md
关键字 : 判断链表是否有环, 找链表入环节点
给定一个链表,判断链表中是否有环。
// 快慢指针
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
// 至少两个节点
ListNode slow = head;
ListNode fast = head;
while (true){
if (fast.next == null || fast.next.next == null)
return false;
else
fast = fast.next.next;
if (slow.val == fast.val)
return true;
else
slow = slow.next;
}
}
- 找出入环节点
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
while (true) {
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if (fast == slow) break;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
公式推导
任意时刻,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
关键字 : 链表 重排序
对链表首尾重排
输入: head = [1,2,3,4,5]
输出: [1,5,2,4,3]
方法一 : 使用List存下每一个节点, 按下标重新连接链表 空间O(n)
略
方法二 : 快慢指针找中点 + 反转后半段链表 + 合并前后两段链表 空间O(1)
public void reorderList(ListNode head) {
if (head == null || head.next == null) return;
ListNode preSlow = new ListNode(-1, head);
// 快慢指针找中点
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next !=null){
fast = fast.next.next;
slow = slow.next;
preSlow = preSlow.next;
}
preSlow.next = null;
// 反转链表后半段
ListNode head2 = reverseList(slow);
// 合并两条链表
merge(head, head2);
// System.out.println(head);
}
// 合并两条链表, 返回合并后的头结点
private void merge(ListNode head1, ListNode head2){
ListNode p = new ListNode(-1);
ListNode x = p;
while (head1 != null && head2 != null){
p.next = head1;
head1 = head1.next;
p = p.next;
p.next = head2;
head2 = head2.next;
p = p.next;
}
if (head1 != null){
p.next = head1;
}
}
// 非递归反转链表, 空间O(1)
public ListNode reverseList(ListNode node) {
if (node == null || node.next == null) return node;
ListNode pre = null;
ListNode cur = node;
while (cur != null) {
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
// 递归反转链表,返回翻转后的头结点
private ListNode reverseList(ListNode node){
if (node == null || node.next == null) return node;
ListNode node1 = reverseList(node.next);
node.next.next = node;
node.next = null;
return node1;
}
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() 操作?
class LRUCache {
// 使用双向链表记录头尾
static class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
// 虚头虚尾
DLinkedNode dummyHead;
DLinkedNode dummyTail;
// 为了快速查询,使用map去记录缓存中所有数据在链表中的位置
Map<Integer, DLinkedNode> map;
// 当前缓存中数据量
int size;
// 缓存容量
int capacity;
// 构造函数
public LRUCache(int capacity) {
// 初始化双向链表
dummyHead = new DLinkedNode();
dummyTail = new DLinkedNode();
dummyHead.next = dummyTail;
dummyTail.prev = dummyHead;
size = 0;
this.capacity = capacity;
map = new HashMap<>(capacity);
}
// 访问缓存, 需要将被访问的元素移到队首
public int get(int key) {
if (map.containsKey(key)) {
int ret = map.get(key).value;
// 当前节点移到链首
moveElementToFirst(map.get(key));
return 1;
}
else
return -1;
}
// 添加元素到缓存
public void put(int key, int value) {
if (map.containsKey(key)){
// 存在, 只修改值
map.get(key).value = value;
}else {
// 不存在, 需新增
if (size == capacity){ // 需要扩容
// 移除链尾元素
// 删除map中对应元素
map.remove(dummyTail.prev.key);
dummyTail.prev = dummyTail.prev.prev;
dummyTail.prev.next = dummyTail;
size--;
}
addElement(key, value);
}
}
public void moveElementToFirst(DLinkedNode node){
// 当前节点从链表上脱离
node.prev.next = node.next;
node.next.prev = node.prev;
// 查到虚头节点后面
node.next = dummyHead.next;
dummyHead.next.prev = node;
node.prev = dummyHead;
dummyHead.next = node;
}
public void addElement(int key, int value){
DLinkedNode node = new DLinkedNode(key, value);
// 节点存入map
map.put(key, node);
// 将节点放到链表最前面
node.next = dummyHead.next;
dummyHead.next.prev = node;
node.prev = dummyHead;
dummyHead.next = node;
size++;
}
}
0148.链表排序.md
关键字 : 链表排序 归并排序 堆排序 优先队列
给你链表的头结点
head
,请将其按 升序 排列并返回 排序后的链表 。在
O(n log n)
时间复杂度下,对链表进行排序
方法一 : 优先队列 堆排序
// 时间复杂度 O(n log n) 空间 O(n)
public ListNode sortList(ListNode head) {
ListNode dummyHead = new ListNode(-1);
PriorityQueue<ListNode> priorityQueue = new PriorityQueue<>((o1, o2) -> {
return o1.val - o2.val;
});
while (head != null){
priorityQueue.add(head);
head = head.next;
}
ListNode p = dummyHead;
while (!priorityQueue.isEmpty()){
p.next = priorityQueue.remove();
p = p.next;
}
p.next = null;
return dummyHead.next;
}
方法二 : 归并排序
// // 时间复杂度 O(n log n) 空间 O(log n) (递归栈的)
private ListNode mergeSort(ListNode head) {
if (head == null || head.next == null) return head;
//利用快慢指针来找到链表的中点
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
// 归并右边
ListNode right = mergeSort(slow.next);
slow.next = null; // 链表从中间断开
// 归并左边
ListNode left = mergeSort(head);
return mergeTwoLists(left, right);
}
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0), cur = dummyHead;
while(l1 != null && l2 != null) {
if(l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
}
else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 != null ? l1 : l2;
return dummyHead.next;
}
0155. 最小栈.md
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素
class MinStack {
Deque<Integer> dataStack;
Deque<Integer> minStack;
/** initialize your data structure here. */
public MinStack() {
dataStack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int x) {
dataStack.addFirst(x);
if (minStack.isEmpty() || minStack.peekFirst() >= x){
minStack.addFirst(x);
}
}
public void pop() {
int popNum = dataStack.removeFirst();
if (!minStack.isEmpty() && popNum == minStack.peekFirst()){
minStack.removeFirst();
}
}
public int top() {
if (dataStack.isEmpty())
throw new RuntimeException("栈空");
return dataStack.peekFirst();
}
public int min() {
if (minStack.isEmpty())
throw new RuntimeException("栈空");
return minStack.peekFirst();
}
}
0160. 相交链表.md
关键字 : 链表 相交
找出链表相交的起始节点, 如果没有交点 返回null
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA, B = headB;
while (A != B) {
if( A != null) A = A.next; else A = headB;
if( B != null) B = B.next; else B = headA;
}
return A;
}
如果只要判断两链表是否相交, 只要判断最后一个尾结点地址是否相同即可
public boolean getIntersectionNode(ListNode headA, ListNode headB) {
while (headA.next != null){
headA = headA.next;
}
while (headB.next != null){
headB = headB.next;
}
return headA == headB;
}
0162. 寻找峰值.md
关键字 : 数组 峰值
峰值元素是指其值大于左右相邻值的元素。
给你一个输入数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以实现时间复杂度为
O(logN)
的解决方案吗?示例
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
// 递归二分
public class Solution {
public int findPeakElement(int[] nums) {
return search(nums, 0, nums.length - 1);
}
public int search(int[] nums, int l, int r) {
if (l == r)
return l;
int mid = (l + r) / 2;
if (nums[mid] > nums[mid + 1])
return search(nums, l, mid);
return search(nums, mid + 1, r);
}
}
// 迭代二分
public class Solution {
public int findPeakElement(int[] nums) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] > nums[mid + 1])
r = mid;
else
l = mid + 1;
}
return l;
}
}
0199.二叉树的右视图.md
关键字 : 二叉树 右侧 层序遍历 BFS
给定一个二叉树的 根节点
root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。示例:
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
public List<Integer> rightSideView(TreeNode root) {
List<Integer> ans = new ArrayList<>();
if (root == null) return ans;
Deque<TreeNode> queue = new LinkedList<>();
queue.addLast(root);
while (!queue.isEmpty()){
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.removeFirst();
if (i == size-1){
// 当前层的最后一个
ans.add(node.val);
}
if (node.left != null) queue.addLast(node.left);
if (node.right != null) queue.addLast(node.right);
}
}
return ans;
}
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
public int numIslands(char[][] grid) {
vis = new boolean[grid.length][grid[0].length];
// 以每一个坐标作为起点坐标
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == '1' && !vis[i][j]){
vis[i][j] = true;
ans++;
dfs(grid, i, j);
}
}
}
return ans;
}
// 访问数组
public boolean[][] vis;
// 行走方向
public int[] xDirection = {1, 0, 0, -1};
public int[] yDirection = {0, -1, 1, 0};
public int ans = 0;
public void dfs(char[][] grid, int row, int col){
for (int i = 0; i < 4; i++) {
int nextX = row+xDirection[i];
int nextY = col+yDirection[i];
if (!(nextX >= grid.length || nextX < 0 || nextY >= grid[0].length || nextY < 0) && !vis[nextX][nextY] && grid[nextX][nextY] == '1') {
vis[nextX][nextY] = true;
dfs(grid, nextX, nextY);
}
}
}
0206.反转链表.md
关键字 : 链表 反转
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。
方法一 : 递归, 空间O(n)
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null){
return head;
}
// 返回了以head.next为头结点的反转后的链表头
ListNode rev = reverseList(head.next);
// 对当前节点进行反转操作
head.next.next = head;
head.next = null;
return rev;
}
方法二 : 用三个指针, 空间O(1)
// 三指针
public static ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode p1 = null;
ListNode p2 = head;
ListNode p3 = head.next;
while (p3 != null){
p2.next = p1;
p1 = p2;
p2 = p3;
p3 =p3.next;
}
p2.next = p1;
return p2;
}
0208. 实现 Trie (前缀树).md
关键词 : 前缀树 字典树 数据结构 单词搜索 字典搜索
0221. 最大正方形.md
在一个由
'0'
和'1'
组成的二维矩阵内,找到只包含'1'
的最大正方形,并返回其面积。输入:matrix = [[“1”,”0”,”1”,”0”,”0”],[“1”,”0”,”1”,”1”,”1”],[“1”,”1”,”1”,”1”,”1”],[“1”,”0”,”0”,”1”,”0”]]
输出:4思路 : 动态规划
我们用 dp(i,j) 表示以 (i,j) 为右下角,且只包含 11 的正方形的边长最大值。如果我们能计算出所有 dp(i,j) 的值,那么其中的最大值即为矩阵中只包含 11 的正方形的边长最大值,其平方即为最大正方形的面积。
对于每个位置 (i,j),检查在矩阵中该位置的值:
- 如果该位置的值是 0,则 dp(i,j)=0,因为当前位置不可能在由 1 组成的正方形中;
- 如果该位置的值是 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
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
int rows = matrix.length, columns = matrix[0].length;
int[][] dp = new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
int maxSquare = maxSide * maxSide;
return maxSquare;
}
0232. 使用栈实现队列.md
使用两个栈实现先入先出队列
class MyQueue {
// 队列长度
private int size;
// 元素入队时入此栈
private Stack<Integer> inQueue;
// 元素出队时从此栈出
private Stack<Integer> outQueue;
public MyQueue() {
inQueue = new Stack<>();
outQueue = new Stack<>();
size=0;
}
/** 将元素x放入队尾 */
public void push(int x) {
inQueue.push(x);
size++;
}
/** 移除队头元素 */
public int pop() {
if (!outQueue.isEmpty()){
size--;
return outQueue.pop();
}else {
// 出栈空
if (inQueue.isEmpty()) // 入栈也空
return -1;
else { // 入栈不空
while (!inQueue.isEmpty()) {
outQueue.push(inQueue.pop());
}
return pop();
}
}
}
/** 查看队头元素 */
public int peek() {
if (!outQueue.isEmpty()){
return outQueue.peek();
}else {
// 出栈空
if (inQueue.isEmpty()) // 入栈也空
return -1;
else { // 入栈不空
while (!inQueue.isEmpty()) {
outQueue.push(inQueue.pop());
}
return peek();
}
}
}
/** 判断队列是否为空 */
public boolean empty() {
return size == 0;
}
}
0236. 二叉树的最近公共祖先.md
关键字 : 二叉树 最近公共祖先 先序遍历 深度优先 DFS
public TreeNode lowestCommonAncestor(TreeNode node, TreeNode p, TreeNode q) {
if (node == null || node == p || node == q)
return node;
// 从左子树中找, 找到其中一个节点就返回, 没找到返回null
TreeNode left = lowestCommonAncestor(node.left, p, q);
// 从右子树中找, 找到其中一个节点就返回, 没找到返回null
TreeNode right = lowestCommonAncestor(node.right, p, q);
// 左右都没找到
if (left == null && right == null)
return null;
// 左边和右边都有, 说明当前node左右各一个,就是祖先节点
else if (left != null && right != null)
return node;
// 否则说明p或q其中一个就是公共祖先
else
return left!=null?left:right;
}
0260. 只出现一次的数字 III.md
关键词 数组 只出现一次的数 位运算 分组异或
给定一个整数数组
nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:[5, 3] 也是有效的答案。
思路, 分组异或
public static int[] singleNumber(int[] nums) {
// 1. 先对数组所有数进行异或操作, 最终结果就是两个单独的数做异或的结果
int temp = 0;
for (int num : nums) {
temp = temp ^ num;
}
// 2. 从右到左对异或结果temp找第一个不为0的位置
int x = 0;
for (int i = 0; i < 32; i++) {
if ((temp>>i & 1) == 1){
x = i;
break;
}
}
// 下面的x代表只有第x位不为0,其余位都是0的二进制数
x = 1<<x;
// 3. 将整个数组里的数分为第x位是0和第x位是1的两部分, 分别异或, 最终答案即为异或结果
int num1 = 0, num2 = 0;
for (int num : nums) {
if ((num & x) == 0){
num1 = num1 ^ num;
}else {
num2 = num2 ^ num;
}
}
return new int[]{num1, num2};
}
0300.最长递增序列.md
关键字 : 数组 最长不连续递增子序列 最长上升子序列 动归
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
思路 : dp[i] 数组表示以 nums[i] 结尾的最长递增子序列
// 时间复杂度O(n^2)
public static int lengthOfLIS(int[] nums) {
int length = nums.length;
int[] dp = new int[length];
dp[0] = 1;
int maxLen = 1;
for (int i = 1; i < nums.length; i++) {
dp[i] = 1; // 初始化为自己形成子序列
// 遍历前面的,找到比自己小的, 更新dp
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]){
dp[i] = Math.max(dp[j]+1, dp[i]);
}
}
maxLen = Math.max(maxLen, dp[i]);
}
return maxLen;
}
0328. 奇偶链表.md
关键词 : 链表 奇偶 重排序
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的时间复杂度应为 O(n), 空间复杂度应为 O(1)
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
public ListNode oddEvenList(ListNode head) {
if (head == null) {
return head;
}
// 偶数链表头
ListNode head2 = head.next;
// 奇数节点指针头
ListNode odd = head;
// 偶数节点指针
ListNode even = head2;
// 拆分成两个链表
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = head2;
return head;
}
0334.递增的三元子序列.md
关键字 : 数组 递增 三元 子序列
给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。
如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。
public boolean increasingTriplet(int[] nums) {
int len = nums.length;
if (len < 3) return false;
// 记录下遍历到的最小值和次小值
int min = Integer.MAX_VALUE, mid = Integer.MAX_VALUE;
for (int num : nums) {
if (num <= min) {
min = num;
} else if (num <= mid) { // 出现了比min大且比mid小的数,更新mid
mid = num;
}
else {
// 一定出现了比mid更大的数
return true;
}
}
return false;
}
0415.字符串相加.md
关键字 : 字符串 加法 相加 模拟 进位
给定两个字符串形式的非负整数
num1
和num2
,计算它们的和。提示:
- num1 和num2 的长度都小于 5100
- num1 和num2 都只包含数字 0-9
- num1 和num2 都不包含任何前导零
- 你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式
public String addStrings(String num1, String num2) {
int num1Len = num1.length();
int num2Len = num2.length();
int maxLen = Math.max(num1Len, num2Len);
StringBuilder ans = new StringBuilder();
// 进位标识
boolean flag = false;
for (int i = 0; i < maxLen; i++) {
int n1 = 0;
int n2 = 0;
int curProd = 0;
if (i < num1Len)
n1 = num1.charAt(num1Len-i-1)-48;
if (i < num2Len)
n2 = num2.charAt(num2Len-i-1)-48;
curProd = flag?n1+n2+1:n1+n2;
flag = curProd >= 10;
ans.insert(0,curProd%10);
}
// 需要判断一下最终flag是否还有进位,有则最前面插入一个1
return flag?ans.insert(0,1).toString():ans.toString();
}
0704.二分查找.md
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
// 防止+操作溢出
int mid = left + (right - left >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target) {
left = mid + 1;
}
else
right = mid - 1;
}
return -1;
}
0912. 快速排序.md
关键字 : 数组 排序 快排
给你一个整数数组
nums
,请你将该数组升序排列。
public void sort(int[] arr, int left, int right){
if (left >= right) return;
int p = partition(arr, left, right);
// 左边排序
sort(arr, left, p-1);
// 右边排序
sort(arr, p+1, right);
}
// 对数组进行分区, 以枢纽为界, 左侧全部小于枢纽, 右侧全部大于枢纽, 返回枢纽最终所在位置
private int partition(int[] arr, int left, int right) {
// 优化点 : 随机选取枢轴元素. 将选中的枢轴元素交换到最左边
int p = left + (new Random()).nextInt(right-left+1);
swap(arr, left, p);
int i = left+1, j = right;
while (true){
// 从左往右找到第一个比枢纽元素大的, 注意循环条件
while (i <= j && arr[i] < arr[left])
i++;
// 从右往左找到第一个比枢纽元素小的,注意循环条件
while (i <= j && arr[j] > arr[left])
j--;
// 循环终止条件
if (i >= j) break;
// 交换两个位置的元素
swap(arr, i, j);
// i往后走一步, j往前走一步, 继续循环
i++;
j--;
}
// 最后交换枢纽元素和j停留的位置
swap(arr, left, j);
return j;
}
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
20. 有效的括号.md
关键词 括号匹配 栈
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。思路 , 只要是左括号, 就入栈, 只要是右括号, 就出栈一个, 判断出栈的字符是否与有括号匹配
public boolean isValid(String s) {
HashMap<Character, Character> map = new HashMap<>();
map.put(')', '(');
map.put(']', '[');
map.put('}', '{');
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
Character c = s.charAt(i);
if (c == '(' || c=='[' || c=='{'){
stack.push(c);
}else {
if (stack.isEmpty()){
return false;
}else {
Character popChar = stack.pop();
if (map.get(c) != popChar){
return false;
}
}
}
}
if (!stack.isEmpty()){
return false;
}
return true;
}
234. 回文链表.md
请判断一个链表是否为回文链表。
要求用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题
思路 : 双指针 + 反转后半部分链表
public static boolean isPalindrome1(ListNode head) {
if (head.next == null) return true;
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
}
// 退出while时, slow指向最中间或者中间左边
ListNode reverseNode = slow.next;
slow.next = null;
ListNode reversedHead = reverse(reverseNode);
while (reversedHead != null){
if (reversedHead.val != head.val){
return false;
}
reversedHead = reversedHead.next;
head = head.next;
}
return true;
}
// 三指针反转链表, 空间O(1)
public static ListNode reverse(ListNode head){
if (head == null || head.next == null) return head;
ListNode pre = null;
ListNode cur = head;
ListNode back = null;
while (cur.next != null){
back = cur.next;
cur.next = pre;
pre = cur;
cur = back;
}
cur.next = pre;
return cur;
}
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变量, 如果需要移除最低访问频次元素, 就从这个变量对应的频次链表中移除.
class LFUCache {
class Node {
int key;
int value;
int freq;
Node pre;
Node next;
public Node() { }
public Node(int key, int value) {
this.key = key;
this.value = value;
this.freq = 1;
}
}
class DLList {
// 头尾伪结点
Node head;
Node tail;
public DLList() {
head = new Node();
tail = new Node();
head.next = tail;
tail.pre = head;
}
void removeNode(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
void addHead(Node node) {
node.next = head.next;
head.next.pre = node;
head.next = node;
node.pre = head;
}
}
Map<Integer, Node> map; // 存储缓存, Map的key为缓存的key
Map<Integer, DLList> freqMap; // 每个频次维护一个双向链表
int size; // 当前元素个数
int capacity; // 允许存储的最大元素个数
int minFreq; // 所有元素中的最小访问频次
public LFUCache(int capacity) {
map = new HashMap<>(capacity);
freqMap = new HashMap<>();
this.capacity = capacity;
this.size = 0;
}
public int get(int key) {
Node node = map.get(key);
if (node == null) {
return -1;
}
// 如果存在, 则对应元素访问频次++
updateFreq(node);
return node.value;
}
public void put(int key, int value) {
if (capacity == 0) return;
Node node = map.get(key);
// 如果key已存在, 则更新value, 并更新其访问频次
if (node != null) {
node.value = value;
updateFreq(node);
} else { // key不存在, 则新增key到访问频次为1的链表中
// 检查是否超出capacity, 超出则删除一个访问频次最低的
if (size == capacity) {
DLList minFreqLL = freqMap.get(minFreq);
// 从缓存中删除
map.remove(minFreqLL.tail.pre.key);
// 从频次链表中删除
minFreqLL.removeNode(minFreqLL.tail.pre); // 这里不需要维护min, 因为下面add了newNode后min肯定是1.
size--;
}
// 将新节点插入访问频次为1的频次链表中
node = new Node(key, value);
map.put(key, node);
DLList ll = freqMap.getOrDefault(1, new DLList());
ll.addHead(node);
freqMap.put(1, ll);
size++;
minFreq = 1;
}
}
// node已存在,更新node的访问频次
void updateFreq(Node node) {
// 从原freq对应的链表里移除
int preFreq = node.freq;
DLList ll = freqMap.get(preFreq);
ll.removeNode(node);
// 如果当前频次链表为最低频次链表, 且从链表中删除node后链表为空时, 需要更新最小访问频次变量
if (preFreq == minFreq && ll.head.next == ll.tail) {
minFreq = preFreq + 1;
}
// 加入新freq对应的链表
int curFreq = preFreq + 1;
node.freq = curFreq;
DLList curDLL = freqMap.getOrDefault(curFreq, new DLList());
curDLL.addHead(node);
freqMap.put(curFreq, curDLL);
}
}
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
public int rand10(){
while (true){
int num = (rand7() - 1) * 7 + rand7();
if (num <= 40)
return num % 10 + 1;
rand10();
}
}
public int rand7(){
Random random = new Random();
return random.nextInt(7) + 1;
}
8. 字符串转换整数 (atoi).md
关键字 : 字符串转整数
函数
myAtoi(string s)
的算法如下:
- 读入字符串并丢弃无用的前导空格
- 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
- 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
- 将前面步骤读入的这些数字转换为整数(即,”123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
- 如果整数数超过 32 位有符号整数范围 [−231, 231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被固定为 −231 ,大于 231 − 1 的整数应该被固定为 231 − 1 。
返回整数作为最终结果。
public static int myAtoi(String s) {
s = s.trim();
if (s.length() == 0) return 0;
boolean isNegative = false; // 负数标记
if (s.charAt(0) == '-'){
isNegative = true;
}
long ans = 0;
for (int i = 0 ; i < s.length(); i++) {
// 如果第一个字符是正负号,则跳过
if (i == 0 && (s.charAt(0) == '-' || s.charAt(0) == '+')){
continue;
}
// 判断当前字符是否是数字, 如果是, 则累加, 如果不是, 则结束遍历
int cur = s.charAt(i) - '0';
if (cur >= 0 && cur <= 9){
ans = ans * 10 + cur;
}else {
break;
}
// 判断是否越界
if (isNegative && ans > Integer.MAX_VALUE){
return Integer.MIN_VALUE;
}else if (ans > Integer.MAX_VALUE){
return Integer.MAX_VALUE;
}
}
return isNegative ? -(int) ans : (int) ans;
}
剑指 Offer 15. 二进制中1的个数.md
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数
思路 : n&(n-1) 位运算可以将 n 的位级表示中最低的那一位 1 设置为 0。不断将 1 设置为 0,直到 n 为 0。
public int NumberOf1(int n) {
int cnt = 0;
while (n != 0) {
cnt++;
n &= (n - 1);
}
return cnt;
}
剑指 Offer 41. 数据流中的中位数.md
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
/* 大顶堆,存储左半边元素 */
private PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1);
/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */
private PriorityQueue<Integer> right = new PriorityQueue<>();
/* 当前数据流读入的元素个数 */
private int N = 0;
public void Insert(Integer val) {
/* 插入要保证两个堆存于平衡状态 */
if (N % 2 == 0) {
/* N 为偶数的情况下插入到右半边。
* 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大,
* 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */
left.add(val);
right.add(left.poll());
} else {
right.add(val);
left.add(right.poll());
}
N++;
}
// 任意时刻获取中位数, 复杂度O(1)
public Double GetMedian() {
if (N % 2 == 0)
return (left.peek() + right.peek()) / 2.0;
else
return (double) right.peek();
}
剑指 Offer 50. 第一个只出现一次的字符.md
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
ASCII 码只有 128 个字符,因此可以使用长度为 128 的整型数组来存储每个字符出现的次数。
注意最后遍历的是字符串, 而不是数组, 因为要保证找到的是第一个出现一次的字符
public static int FirstNotRepeatingChar(String str) {
// 128长度数组存储字符出现的次数
int[] cnts = new int[128];
// 计算每个字符出现的次数
for (int i = 0; i < str.length(); i++)
cnts[str.charAt(i)]++;
// 从头到尾再遍历一遍字符串, 看哪个字符的出现次数等于1
for (int i = 0; i < str.length(); i++)
if (cnts[str.charAt(i)] == 1)
return i;
return -1;
}
剑指 Offer 51. 数组中的逆序对.md
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
思路 : 利用归并排序过程发现逆序对
// 答案
private int res;
public int reversePairs(int[] nums){
res = 0;
mergeSort(nums);
return res;
}
public void mergeSort(int[] arr){
int[] temp = new int[arr.length];
sort(arr, 0, arr.length-1, temp);
}
public void sort(int[] arr, int left, int right, int[] temp){
if (left>=right){
return;
}
int mid = (left+right)/2;
sort(arr, left, mid, temp);
sort(arr, mid+1, right, temp);
if (arr[mid] > arr[mid+1])
merge(arr, left, mid, right, temp);
}
// 归并过程
private void merge(int[] arr, int left, int mid, int right, int[] temp) {
System.arraycopy(arr, left, temp, left, right-left+1);
// 双指针比较并移动
int i = left, j = mid+1;
for (int k = left; k <= right; k++) {
if (i > mid){
arr[k] = temp[j];
j++;
}else if (j > right){
arr[k] = temp[i];
i++;
}else if (temp[i] <= temp[j]){
arr[k] = temp[i];
i++;
}else {
arr[k] = temp[j];
// 最关键的地方! 如果temp[i] > temp[j], 即左边指针位置元素大于右边, 则左边所有剩余元素都与右边指针位置元素形成逆序对
res += (mid-i+1);
j++;
}
}
}
剑指 Offer 59 - I. 滑动窗口的最大值.md
给定一个数组
nums
和滑动窗口的大小k
,请找出所有滑动窗口里的最大值。
方法一 : 优先队列 大顶堆
public ArrayList<Integer> maxInWindows(int[] num, int size) {
ArrayList<Integer> ans = new ArrayList<>();
if (size > num.length || size < 1)
return ans;
// 大顶堆, 堆顶是最大值
PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);
for (int i = 0; i < size; i++)
heap.add(num[i]);
ans.add(heap.peek());
/* 维护一个大小为 size 的大顶堆 */
for (int i = 0, j = i + size; j < num.length; i++, j++) {
// 滑出窗口的数出堆, 新加入窗口的数入堆
heap.remove(num[i]);
heap.add(num[j]);
// 取新窗口中的最大值
ans.add(heap.peek());
}
return ans;
}
方法二 : 单调队列
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
for(int j = 0, i = 1 - k; j < nums.length; i++, j++) {
// 删除 deque 中对应的 nums[i-1]
if(i > 0 && deque.peekFirst() == nums[i - 1])
deque.removeFirst();
// 保持 deque 递减
while(!deque.isEmpty() && deque.peekLast() < nums[j])
deque.removeLast();
deque.addLast(nums[j]);
// 记录窗口最大值
if(i >= 0)
res[i] = deque.peekFirst();
}
return res;
}
}
剑指 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,2,3,4,5]
输出: [120,60,40,30,24]
public int[] constructArr(int[] a) {
int len = a.length;
if(len == 0) return new int[0];
// 声明两个数组, 一个保存A[i]左边所有数的累乘和
int[] dp1 = new int[len];
// 一个保存A[i]右边所有数的累乘和
int[] dp2 = new int[len];
// 累乘A[i]左边的数
dp1[0] = 1;
for (int i = 1; i < len; i++) {
dp1[i] = dp1[i - 1] * a[i - 1];
}
// 累乘A[i]右边的数
dp2[len - 1] = 1;
for (int i = len - 2; i >= 0; i--) {
dp2[i] = dp2[i + 1] * a[i + 1];
}
// 求B[i] = 左边 * 右边
for (int i = 0; i < len; i++) {
a[i] = dp1[i] * dp2[i];
}
return a;
}
剑指 Offer II 001. 整数除法.md
给定两个整数
a
和b
,求它们的除法的商a/b
,要求不得使用乘号'*'
、除号'/'
以及求余符号'%'
。
示例:
10 / 3 = 3
7 / -3 = -2
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 )
public int divide(int dividend, int divisor) {
// 特殊情况处理, 面试时先不写, 一般很难考虑到这一层
if (dividend == Integer.MIN_VALUE && divisor == -1) return Integer.MAX_VALUE;
// 将两数都转为正整数处理
boolean isNegative = (dividend < 0 && divisor > 0) || (dividend > 0 && divisor < 0);
long a = Math.abs((long) dividend);
long b = Math.abs((long) divisor);
int ans = 0;
int shift = 31;
// a 每次减去 b移shift位, 相当于 a - b * 2^shift
while (a >= b){
while (a < b << shift){
shift--;
}
a -= b << shift;
ans += 1 << shift;
}
return isNegative ? -ans : ans;
}
剑指 Offer II 002. 二进制加法.md
给定两个 01 字符串
a
和b
,请计算它们的和,并以二进制字符串的形式输出。输入为 非空 字符串且只包含数字
1
和0
。
输入: a = "1010", b = "1011"
输出: "10101"
思路 : 模拟,设置是否要进位的标志变量flag
public static String addBinary(String a, String b) {
// 模拟
int aLen = a.length(), bLen = b.length();
int maxLen = Math.max(aLen, bLen);
boolean flag = false;
StringBuilder ans = new StringBuilder();
for (int i = 0; i < maxLen; i++) {
char aChar = '0';
char bChar = '0';
if (aLen-i-1 >= 0) aChar=a.charAt(aLen-i-1);
if (bLen-i-1 >= 0) bChar=b.charAt(bLen-i-1);
int cur = aChar - '0' + bChar - '0';
if (flag) cur++;
if (cur > 1){ // 要进位了
flag = true;
cur = cur % 2;
}else {
flag = false;
}
ans.insert(0, cur);
}
if (flag) ans.insert(0, '1');
return ans.toString();
}
剑指 Offer II 003. 前 n 个数字二进制中 1 的个数.md
关键词 : 二进制 动态规划
给定一个非负整数
n
,请计算0
到n
之间的每个数字的二进制表示中 1 的个数,并输出一个数组。每次计算一个数包含多少个1时,只需要将该数往右移动一位(等价于除2),此时有两种情况:
如果计算的是偶数,1的个数 = 移位后的数中1的个数
如果计算的是奇数,1的个数 = 移位后的数中1的个数 + 1 (因为移位前末尾有个1)
public int[] countBits(int n) {
int[] ret = new int[n+1];
for (int i = 1; i <= n; i++) {
ret[i] = ret[i>>1];
if (i % 2 == 1) ret[i]++;
}
return ret;
}
func countBits(n int) []int {
ret := make([]int, n+1)
for i := 1; i <= n; i++ {
ret[i] = ret[i>>1]
if i%2 == 1 {
ret[i]++
}
}
return ret
}
剑指 Offer II 004. 只出现一次的数字 .md
关键词 : 数组 只出现一次的数 位运算
给你一个整数数组
nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。思路 : 累加每个数字每一个bit位上的1, 最后对每一位上1的个数对3取余数, 就得到了目标答案在这一位上是0还是1
public int singleNumber(int[] nums) {
int[] bitCounts = new int[32];
// 对每个数字进行位移, 累加每一位上的1
for (int num : nums) {
for (int i = 31; i >= 0; i--) {
if ((num & 1) == 1) {
bitCounts[i]++;
}
num = num >> 1;
}
}
// 遍历记录数组, 每一位对3取余, 得到目标值每一位上的值
int ans = 0;
for (int i = 0; i < 32; i++) {
ans = ans<<1;
ans += bitCounts[i] % 3;
}
return ans;
}
剑指 Offer II 005. 单词长度的最大乘积.md
给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。
输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]
输出: 16
解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。
输入: words = ["a","aa","aaa","aaaa"]
输出: 0
解释: 不存在这样的两个单词。
思路,比较挫,暴力遍历每一个字符串,比较是否有相同字符,如果没有则更新maxProd
时间复杂度 O(n^2 + L1+L2)
public int maxProduct(String[] words) {
int maxProd = 0;
for (int i = 0; i < words.length; i++) {
for (int j = i+1; j < words.length; j++) {
if (!containSameChar(words[i], words[j])){
maxProd = Math.max(maxProd, words[i].length() * words[j].length());
}
}
}
return maxProd;
}
public boolean containSameChar(String word1, String word2){
boolean[] arr = new boolean[26];
for (int i = 0; i < word1.length(); i++) {
arr[word1.charAt(i)-'a'] = true;
}
for (int i = 0; i < word2.length(); i++) {
if (arr[word2.charAt(i) - 'a'])
return true;
}
return false;
}
剑指 Offer II 006. 排序数组中两个数字之和.md
给定一个已按照 升序排列 的整数数组
numbers
,请你从数组中找出两个数满足相加之和等于目标数target
。假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。
输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。思路 : 双指针, 一头一尾,慢慢向中间靠拢
public int[] twoSum(int[] numbers, int target) {
int index1 = 0, index2 = numbers.length-1;
while (true){
int curSum = numbers[index1] + numbers[index2];
if (curSum == target){
return new int[]{index1 , index2};
}else if (curSum < target){
index1++;
}else {
index2--;
}
}
}
剑指 Offer II 008. 和大于等于 target 的最短子数组.md
关键词 数组 连续子数组 最小长度 滑动窗口
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
思路, 滑动窗口
public int minSubArrayLen1(int s, int[] nums) {
int left = 0, right = 0;
int curSum = 0, ans = Integer.MAX_VALUE;
while (right < nums.length) {
curSum += nums[right++];
while (curSum >= s) {
ans = Math.min(ans, right - left);
curSum -= nums[left++];
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
多线程交替打印.md
public class 多线程交替输出 {
public static void main(String[] args) {
// 推荐使用方法一和三,最简单
method3();
}
// 方法一
public static void method1(){
WaitNotify waitNotify = new WaitNotify(1, 5);
new Thread(()->waitNotify.print("hello1", 1, 2),"t1").start();
new Thread(()->waitNotify.print("hello2", 2, 3),"t2").start();
new Thread(()->waitNotify.print("hello3", 3, 1),"t3").start();
}
// 方法二
public static void method2(){
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition t1 = awaitSignal.newCondition();
Condition t2 = awaitSignal.newCondition();
Condition t3 = awaitSignal.newCondition();
new Thread(()->awaitSignal.print("hello1", t1, t2), "t1").start();
new Thread(()->awaitSignal.print("hello2", t2, t3), "t2").start();
new Thread(()->awaitSignal.print("hello3", t3, t1), "t3").start();
awaitSignal.lock();
try {
t1.signal();
} finally {
awaitSignal.unlock();
}
}
// 方法三
static Thread t1;
static Thread t2;
static Thread t3;
public static void method3(){
ParkUnpark parkUnpark = new ParkUnpark(5);
t1 = new Thread(() -> parkUnpark.print("hello1", t2));
t2 = new Thread(() -> parkUnpark.print("hello2", t3));
t3 = new Thread(() -> parkUnpark.print("hello3", t1));
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
// 第一种方法, 使用Wait() NotifyAll()
class WaitNotify{
private int flag;
private int loopNumber;
public void print(String str, int flag, int nextFlag){
for (int i = 0; i < this.loopNumber; i++) {
synchronized (this){
while (this.flag != flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " : " + str);
this.flag = nextFlag;
this.notifyAll();
}
}
}
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
}
// 方法二 : 使用ReentrantLock的Await 和 Signal
class AwaitSignal extends ReentrantLock{
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str, Condition current, Condition next){
for (int i = 0; i < this.loopNumber; i++) {
this.lock();
try {
// 一上来先进阻塞队列
current.await();
System.out.println(Thread.currentThread().getName() + " : " + str);
// 打印完,唤醒下一个
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
}
// 方法三 : 使用Park Unpark
class ParkUnpark{
private int loopNumber;
public void print(String str, Thread next){
for (int i = 0; i < this.loopNumber; i++) {
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " : " + str);
LockSupport.unpark(next);
}
}
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
}
奇升偶降链表.md
字节跳动高频题——排序奇升偶降链表
给定一个奇数位升序,偶数位降序的链表,将其重新排序。
输入: 1->8->3->6->5->4->7->2->NULL
输出: 1->2->3->4->5->6->7->8->NULL
思路 :
- 按奇偶位置拆分链表, 得1->3->5->7->NULL和8->6->4->2->NULL
- 反转偶链表,得1->3->5->7->NULL和2->4->6->8->NULL
- 合并两个有序链表,得1->2->3->4->5->6->7->8->NULL
时间复杂度为O(N),空间复杂度O(1)。
public ListNode oddEvenList(ListNode head) {
if (head == null) {
return head;
}
// 偶数链表头
ListNode head2 = head.next;
// 奇数节点指针头
ListNode odd = head;
// 偶数节点指针
ListNode even = head2;
// 拆分成两个链表
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = null;
// 反转偶数链表
ListNode newHead2 = reverse(head2);
// 链表合并
return mergeTwoLists(head, newHead2);
}
// 合并两个有序链表
public ListNode mergeTwoLists(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(-1);
// p指针指向合并后的最后一个节点
ListNode p = dummyHead;
while (head1 != null && head2 != null) {
if (head1.val <= head2.val) {
p.next = head1;
head1 = head1.next;
} else {
p.next = head2;
head2 = head2.next;
}
p = p.next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
p.next = head1 == null ? head2 : head1;
return dummyHead.next;
}
// 迭代反转链表
private ListNode reverse(ListNode head) {
if (head == null || head.next == null) return head;
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
手撕单例模式.md
懒汉式(DCL : double check lock)
class LazySingleton{
private static volatile LazySingleton instance;
private LazySingleton(){}
public static LazySingleton getInstance() {
if (instance == null){
synchronized (LazySingleton.class){
if (instance == null)
instance = new LazySingleton();
}
}
return instance;
}
}
饿汉式
class HungrySingleton{
private static HungrySingletonTest instance = new HungrySingletonTest();
private HungrySingleton(){}
public static HungrySingletonTest getInstance() {
return instance;
}
}
手撕归并排序.md
public void mergeSort(int[] arr){
// temp是一个辅助归并的空数组
int[] temp = new int[arr.length];
sort(arr, 0, arr.length-1, temp);
}
public void sort(int[] arr, int left, int right, int[] temp){
if (left>=right){
return;
}
int mid = (left+right)/2;
sort(arr, left, mid, temp);
sort(arr, mid+1, right, temp);
// 优化点, 面试中可以后面补充, 如果左边最后一个小于或等于右边第一个则不用归并
if (arr[mid] > arr[mid+1])
merge(arr, left, mid, right, temp);
}
// 归并过程
private void merge(int[] arr, int left, int mid, int right, int[] temp) {
// 归并时, 将原数组中需要归并的部分复制到辅助数组中
System.arraycopy(arr, left, temp, left, right-left+1); // 优化点
// 双指针比较并移动
int i = left, j = mid+1;
for (int k = left; k <= right; k++) {
// 如果左边归并完了, 直接放入右边的元素即可
if (i > mid){
arr[k] = temp[j];
j++;
}else if (j > right){ // 如果右边归并完了, 直接放入左边的元素即可
arr[k] = temp[i];
i++;
}else if (temp[i] <= temp[j]){ // 取二者之中较小
arr[k] = temp[i];
i++;
}else {
arr[k] = temp[j];
j++;
}
}
}
手撕线程池.md
关键在阻塞队列
class ThreadPool{
// 核心线程数
private int coreSize;
// 阻塞队列(任务集合)
private BlockingDeque<Runnable> taskQueue;
private int queueCapacity;
// 线程集合
private HashSet<Worker> workers;
public ThreadPool(int coreSize, int queueCapacity) {
this.coreSize = coreSize;
this.taskQueue = new LinkedBlockingDeque<>(queueCapacity);
this.queueCapacity = queueCapacity;
this.workers = new HashSet<>();
}
// 执行任务
public void execute(Runnable task){
synchronized (workers) {
// 1. 判断当前线程数是否等于coreSize
if (workers.size() < coreSize){
// 若小于, 则创建新线程执行任务
Worker worker = new Worker(task);
worker.start();
// worker 加入线程集合
workers.add(worker);
}else{
// 若等于,则放入阻塞队列
// 情况一: 阻塞队列不满, 直接添加
if (taskQueue.size() < queueCapacity) {
System.out.println("加入任务队列");
taskQueue.add(task);
}
else {
System.out.println("任务被抛弃...");
}
// 情况二: 阻塞队列满, 拒绝策略(啥也不做)
}
}
}
// 线程包装类
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 执行任务
// task 任务执行完毕后, 或者task为空时, 从任务队列中取出任务执行
while (task != null || !taskQueue.isEmpty()){
try {
if (task == null)
task = taskQueue.take();
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
// 任务队列中没有任务, 则销毁当前worker
synchronized (workers) {
workers.remove(this);
}
}
}
}