Skip to content

Commit 9d63e9b

Browse files
committed
📖juc
1 parent e023553 commit 9d63e9b

File tree

12 files changed

+343
-64
lines changed

12 files changed

+343
-64
lines changed

docs/.DS_Store

0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

docs/data-structure-algorithms/algorithm/Dynamic-Programming.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ categories: Algorithm
9797

9898
### 斐波那契数列
9999

100-
PS:我们先从一个简单的斐波那契数列来进一步理解下重叠子问题与状态转移方程(斐波那契数列并不是严格意义上的动态规划,因为它没有求最值,所以也没设计到最优子结构的问题
100+
PS:我们先从一个简单的斐波那契数列来进一步理解下重叠子问题与状态转移方程(斐波那契数列并不是严格意义上的动态规划,因为它没有求最值,所以也没涉及到最优子结构的问题
101101

102102
**1、暴力递归**
103103

@@ -647,22 +647,25 @@ public int coinChange(int[] coins, int amount) {
647647

648648
//初始值
649649
dp[0] = 0;
650-
// 外层 for 循环在遍历所有状态的所有取值
650+
// 外层 for 循环在遍历所有可能得金额,(从1到amount)
651651
//dp[i]上的值不断选择已含有硬币值当前位置的数组值 + 1,min保证每一次保存的是最小值
652652
for (int i = 1; i < amount + 1; i++) {
653-
//内层 for 循环在求所有选择的最小值 状态转移方程
653+
//内层循环所有硬币面额
654654
for (int coin : coins) {
655+
//如果i<coin,当前硬币coin面额太大,无法凑成金额i
655656
if (i >= coin) {
656657
//分两种情况,使用硬币coin和不使用,取最小值
657658
dp[i] = Math.min(dp[i - coin] + 1, dp[i]);
658659
}
659660
}
660661
}
661-
return dp[amount] > amount ? -1 : dp[amount];
662+
return dp[amount] == amount + 1 ? -1 : dp[amount];
662663
}
663664
```
664665

665666
> 为啥 `dp` 数组初始化为 `amount + 1` 呢,因为凑成 `amount` 金额的硬币数最多只可能等于 `amount`(全用 1 元面值的硬币),所以初始化为 `amount + 1` 就相当于初始化为正无穷,便于后续取最小值。为啥不直接初始化为 int 型的最大值 `Integer.MAX_VALUE` 呢?因为后面有 `dp[i - coin] + 1`,这就会导致整型溢出。
667+
>
668+
> 最终,dp[amout] 就是凑成总金额所需的最少硬币数,如果dp[amount] 仍是初始化的较大值,说明无法凑出,返回 -1。
666669
667670

668671

@@ -692,7 +695,7 @@ public int coinChange(int[] coins, int amount) {
692695
我们需要找出给定数组中两个数字之间的最大差值(即,最大利润)。此外,第二个数字(卖出价格)必须大于第一个数字(买入价格)
693696
694697
```java
695-
public static int dp(int[] prices) {
698+
public int dp(int[] prices) {
696699
int length = prices.length;
697700
if (length == 0) {
698701
return 0;
@@ -853,8 +856,6 @@ class Solution{
853856

854857
### 动态规划与其它算法的关系
855858

856-
这一章我们将会介绍分治和贪心算法的核心思想,并与动态规划算法进行比较。
857-
858859
#### 分治
859860

860861
解决分治问题的时候,思路就是想办法把问题的规模减小,有时候减小一个,有时候减小一半,然后将每个小问题的解以及当前的情况组合起来得出最终的结果。例如归并排序和快速排序,归并排序将要排序的数组平均地分成两半,快速排序将数组随机地分成两半。然后不断地对它们递归地进行处理。

docs/data-structure-algorithms/soultion/Array-Solution.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,3 +686,192 @@ public void merge(int[] nums1, int m, int[] nums2, int n) {
686686
}
687687
```
688688
689+
690+
691+
### [136. 只出现一次的数字](https://leetcode.cn/problems/single-number/)
692+
693+
> 给你一个 **非空** 整数数组 `nums` ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
694+
>
695+
> 你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
696+
>
697+
> ```
698+
> 输入:nums = [4,1,2,1,2]
699+
>
700+
> 输出:4
701+
> ```
702+
703+
思路:**异或运算**
704+
705+
异或运算(`^`)有以下几个重要性质:
706+
707+
1. 任何数和 `0` 做异或运算,结果仍然是原来的数,即 `a ^ 0 = a`。
708+
2. 任何数和其自身做异或运算,结果是 `0`,即 `a ^ a = 0`。
709+
3. 异或运算满足交换律和结合律,即 `a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c`。
710+
711+
```java
712+
public int singleNumber(int[] nums) {
713+
int result = 0;
714+
for (int num : nums) {
715+
result ^= num;
716+
}
717+
return result;
718+
}
719+
```
720+
721+
722+
723+
### [169. 多数元素](https://leetcode.cn/problems/majority-element/)
724+
725+
> 给定一个大小为 `n` 的数组 `nums` ,返回其中的多数元素。多数元素是指在数组中出现次数 **大于** `⌊ n/2 ⌋` 的元素。
726+
>
727+
> 你可以假设数组是非空的,并且给定的数组总是存在多数元素。
728+
>
729+
> ```
730+
> 输入:nums = [2,2,1,1,1,2,2]
731+
> 输出:2
732+
> ```
733+
734+
思路:排序后取中间元素,如果将数组 `nums` 中的所有元素按照单调递增或单调递减的顺序排序,那么下标为 ⌊2*n*⌋ 的元素(下标从 `0` 开始)一定是众数。
735+
736+
```java
737+
class Solution {
738+
public int majorityElement(int[] nums) {
739+
Arrays.sort(nums);
740+
return nums[nums.length / 2];
741+
}
742+
}
743+
```
744+
745+
746+
747+
### [75. 颜色分类](https://leetcode.cn/problems/sort-colors/)
748+
749+
> 给定一个包含红色、白色和蓝色、共 `n` 个元素的数组 `nums`**[原地](https://baike.baidu.com/item/原地算法)** 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
750+
>
751+
> 我们使用整数 `0``1``2` 分别表示红色、白色和蓝色。
752+
>
753+
> 必须在不使用库内置的 sort 函数的情况下解决这个问题。
754+
>
755+
> ```
756+
> 输入:nums = [2,0,2,1,1,0]
757+
> 输出:[0,0,1,1,2,2]
758+
> ```
759+
760+
思路:双指针(荷兰国旗问题)。用 3 个指针遍历数组,过程中,移动 left 、right 和 curr 指针即可。
761+
762+
```java
763+
public void sortColors(int[] nums) {
764+
// 初始化三个指针
765+
int left = 0; // 左指针,指向当前遍历到的最左边的一个0的右侧
766+
int right = nums.length - 1; // 右指针,指向当前遍历到的最右边的一个2的左侧
767+
int curr = 0; // 当前遍历指针
768+
769+
// 遍历数组
770+
while (curr <= right) {
771+
// 如果当前元素是0,则将其与左指针指向的元素交换,并将左指针和右指针都向右移动一位
772+
if (nums[curr] == 0) {
773+
swap(nums, curr, left);
774+
left++;
775+
curr++;
776+
}
777+
// 如果当前元素是2,则将其与右指针指向的元素交换,但只将右指针向左移动一位
778+
// 因为交换过来的元素可能是0或1,需要再次判断
779+
else if (nums[curr] == 2) {
780+
swap(nums, curr, right);
781+
right--;
782+
}
783+
// 如果当前元素是1,则只需将当前指针向右移动一位
784+
else {
785+
curr++;
786+
}
787+
}
788+
}
789+
790+
// 交换数组中两个元素的位置
791+
private void swap(int[] nums, int i, int j) {
792+
int temp = nums[i];
793+
nums[i] = nums[j];
794+
nums[j] = temp;
795+
}
796+
```
797+
798+
799+
800+
### [287. 寻找重复数](https://leetcode.cn/problems/find-the-duplicate-number/)
801+
802+
> 给定一个包含 `n + 1` 个整数的数组 `nums` ,其数字都在 `[1, n]` 范围内(包括 `1``n`),可知至少存在一个重复的整数。
803+
>
804+
> 假设 `nums` 只有 **一个重复的整数** ,返回 **这个重复的数**
805+
>
806+
> 你设计的解决方案必须 **不修改** 数组 `nums` 且只用常量级 `O(1)` 的额外空间。
807+
>
808+
> ```
809+
> 输入:nums = [1,3,4,2,2]
810+
> 输出:2
811+
> ```
812+
813+
**思路**:由于题目要求不能修改原数组(不能排序),且空间复杂度为$O(1)$,时间复杂度低于$O(n^2)$,我们可以使用二分查找或者快慢指针(循环检测)的方法。
814+
815+
1. 二分查找(基于抽屉原理)
816+
817+
抽屉原理:如果有 n 个抽屉和 n+1 个物品,那么至少有一个抽屉有至少两个物品。
818+
819+
我们不是在原始数组上进行二分,而是在数字范围上进行二分。数字的范围是 [1, n]。
820+
对于中间数 mid,统计数组中 <= mid 的元素的个数 count:
821+
822+
- 如果 count <= mid,那么重复元素一定在 [mid+1, n] 之间。
823+
824+
- 否则,重复元素在 [1, mid] 之间。
825+
826+
```java
827+
public int findDuplicate(int[] nums) {
828+
int left = 1;
829+
int right = nums.length - 1;
830+
while (left < right) {
831+
int mid = left + (right - left) / 2;
832+
int count = 0;
833+
for (int num : nums) {
834+
if (num <= mid) {
835+
count++;
836+
}
837+
}
838+
if (count > mid) {
839+
right = mid;
840+
} else {
841+
left = mid + 1;
842+
}
843+
}
844+
return left;
845+
}
846+
```
847+
848+
2. 快慢指针法
849+
850+
把数组`nums`看成一个特殊的链表,数组的索引是链表节点的编号,数组的值是指向下一个节点的指针。因为存在重复数字,所以链表中必然存在环,那么问题就转化为寻找链表环的入口,该入口对应的数字就是重复的数。
851+
852+
- 初始化两个指针`slow``fast`,都指向数组的第一个元素,即`nums[0]`
853+
-`slow`指针每次走一步(`slow = nums[slow]`),`fast`指针每次走两步(`fast = nums[nums[fast]]`),直到`slow``fast`相遇。
854+
- 相遇后,将`fast`指针重新指向`nums[0]`,然后`slow``fast`指针每次都走一步,当它们再次相遇时,相遇点对应的数字就是重复的数。
855+
856+
```java
857+
public int findDuplicate(int[] nums) {
858+
int slow = nums[0];
859+
int fast = nums[0];
860+
// 第一轮,找到相遇点
861+
do {
862+
slow = nums[slow];
863+
fast = nums[nums[fast]];
864+
} while (slow != fast);
865+
866+
// 重置其中一个指针到起点
867+
fast = nums;
868+
// 两个指针都每次一步移动,直到相遇
869+
while (slow != fast) {
870+
slow = nums[slow];
871+
fast = nums[fast];
872+
}
873+
return slow; // 或者fast,此时它们相等
874+
}
875+
```
876+
877+

docs/data-structure-algorithms/soultion/DP-Solution.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1+
---
2+
title: 动态规划 通关秘籍
3+
date: 2025-07-08
4+
tags:
5+
- DP
6+
categories: leetcode
7+
---
8+
19
> 个人感觉动态规划是最难的,一会爬楼梯,一会兑零钱,一会又要去接雨水,炒股就不说了,还要去偷东西,哎,我太南了
210
311
![](https://cdn.nlark.com/yuque/0/2021/png/21674094/1639551516595-9b6a2bad-c55b-43e1-b172-ced36ffa96cc.png)
412

513
## 子序列问题
614

7-
一旦涉及到子序列和最值,那几乎可以肯定,**考察的是动态规划技巧,时间复杂度一般都是 O(n^2)**
15+
一旦涉及到子序列和最值,那几乎可以肯定,**考察的是动态规划技巧,时间复杂度一般都是 $O(n^2)$**
816

917
两种思路
1018

1119
**1、第一种思路模板是一个一维的 dp 数组**
1220

13-
```
21+
```java
1422
int n = array.length;
1523
int[] dp = new int[n];
1624

@@ -53,18 +61,15 @@ for (int i = 0; i < n; i++) {
5361
5462
PS: 注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的
5563
56-
> https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-2/
57-
58-
这种题目看懂需要看动图
59-
60-
![img](https://labuladong.online/algo/images/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/gif1.gif)
64+
![](https://writings.sh/assets/images/posts/algorithm-longest-increasing-subsequence/longest-increasing-subsequence-dp1-2.jpeg)
6165
6266
```java
63-
public static int getLengthOfLIS(int[] nums) {
67+
public int getLengthOfLIS(int[] nums) {
6468
6569
int[] dp = new int[nums.length];
6670
Arrays.fill(dp, 1);
6771
72+
int maxLength = 1;
6873
for (int i = 0; i < nums.length; i++) {
6974
for (int j = 0; j < i; j++) {
7075
//当 nums[i] <= nums[j] 时: nums[i] 无法接在 nums[j]之后,此情况上升子序列不成立,跳过,不是比较dp[i]和dp[j]
@@ -76,10 +81,7 @@ PS: 注意「子序列」和「子串」这两个名词的区别,子串一
7681
dp[i] = Math.max(dp[i], dp[j] + 1);
7782
}
7883
}
79-
}
80-
int res = 0;
81-
for (int i = 0; i < len; i++) {
82-
res = Math.max(res, dp[i]);
84+
maxLength = Math.max(maxLength, dp[i]);
8385
}
8486
return res;
8587
}

docs/interview/Algorithm.md

Lines changed: 0 additions & 15 deletions
This file was deleted.

docs/interview/JUC-FAQ.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ Java 通过 `Thread.State` 枚举定义了线程的**6 种状态**(JDK 1.5 后
307307

308308
### 说说 sleep() 方法和 wait() 方法区别和共同点?
309309

310-
- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁**
311-
- 两者都可以暂停线程的执行。
310+
sleep () 和 wait () 的核心区别在于锁的处理机制:
311+
312+
1. **锁释放**:sleep () 不释放锁,wait () 释放锁并进入等待队列;
313+
2. **唤醒方式**:sleep () 依赖时间或中断,wait () 依赖其他线程通知;
314+
3. **使用场景**:sleep () 用于线程暂停,wait () 用于线程协作(wait 方法必须在 synchronized 保护的代码中使用)。
315+
316+
317+
312318
- `wait ()`通常被用于线程间交互/通信(wait 方法必须在 synchronized 保护的代码中使用),sleep 通常被用于暂停执行。
313319
- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify()` 或者 `notifyAll()` 方法。`sleep()` 方法执行完成后,线程会自动苏醒。或者可以使用 `wait(long timeout)` 超时后线程会自动苏醒。
314320

@@ -320,6 +326,14 @@ Java 通过 `Thread.State` 枚举定义了线程的**6 种状态**(JDK 1.5 后
320326
321327

322328

329+
#### 为什么 wait () 必须在 synchronized 块中?
330+
331+
- **原子性保障**:避免线程安全问题(如生产者修改队列后,消费者未及时感知)。
332+
333+
- **JVM 实现机制**:锁对象的 `monitor` 记录等待线程,需通过 `synchronized` 获取锁后才能操作 `monitor`
334+
335+
336+
323337
### 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
324338

325339
调用 `start()` 方法最终会导致在新的执行路径上执行 `run()` 方法中的代码,这是 Java 实现多线程的标准方式。直接调用 `run()` 方法通常是一个错误,原因在于两者在行为、线程生命周期和底层执行机制上存在根本区别:
@@ -1413,11 +1427,11 @@ LongAdder 引入了分段累加的概念,内部一共有两个参数参与计
14131427
### 为什么要用线程池,优势是什么?
14141428
14151429
> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。**
1416-
1417-
如果每个任务都创建一个线程会带来哪些问题:
1418-
1419-
1. 第一点,反复创建线程系统开销比较大,每个线程创建和销毁都需要时间,如果任务比较简单,那么就有可能导致创建和销毁线程消耗的资源比线程执行任务本身消耗的资源还要大。
1420-
2. 第二点,过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。
1430+
>
1431+
> 如果每个任务都创建一个线程会带来哪些问题:
1432+
>
1433+
> 1. 第一点,反复创建线程系统开销比较大,每个线程创建和销毁都需要时间,如果任务比较简单,那么就有可能导致创建和销毁线程消耗的资源比线程执行任务本身消耗的资源还要大。
1434+
> 2. 第二点,过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。
14211435
14221436
线程池是一种基于池化思想管理线程的工具。
14231437

docs/interview/Java-Basics-FAQ.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,17 @@ Exception 又分为**可检查**(checked)异常和**不检查**(unchecked
107107

108108
面向对象 ( Object Oriented ) 是将现实问题构建关系,然后抽象成 **类 ( class )**,给类定义属性和方法后,再将类实例化成 **实例 ( instance )** ,通过访问实例的属性和调用方法来进行使用。
109109

110-
类具有三个基本特征:封装、继承、多态
110+
类具有基本特征:封装、继承、多态、抽象
111+
112+
设计原则:
113+
114+
| 原则 | 核心思想 | 典型应用 |
115+
| ------------ | ---------------------------- | ------------------------------------------------- |
116+
| **单一职责** | 一个类只做一件事 | 拆分`UserManager``AuthService`+`ProfileService` |
117+
| **开闭原则** | 对扩展开放,对修改关闭 | 通过策略模式实现支付方式扩展 |
118+
| **里氏替换** | 子类不破坏父类契约 | `Bird`类不应继承`Penguin`(企鹅不会飞) |
119+
| **接口隔离** | 多个专用接口优于单一臃肿接口 | 拆分`Animal``Flyable`/`Swimmable` |
120+
| **依赖倒置** | 依赖抽象而非实现 | 订单模块通过`PaymentGateway`接口调用支付 |
111121

112122

113123

0 commit comments

Comments
 (0)