update string
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
# 红黑树 (red-black tree)
|
||||
|
||||
红黑树(Red Black Tree) 是一种自平衡二叉查找树。一种特化的AVL树,在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。 它可以在O(log n)时间内做查找,插入和删除。
|
||||
|
||||
它的每个结点都被“着色”为红色或者黑色,这些结点的颜色被用来检测树的平衡性。
|
||||
|
||||
|
||||
### 应用场景
|
||||
|
||||
java 中 HashMap、TreeMap 的底层实现,当HashMap中元素大于8个时,HashMap底层存储实现改为红黑树,以提高元素搜索速度。
|
||||
* java 中 HashMap、TreeMap 的底层实现,当HashMap中元素大于8个时,HashMap底层存储实现改为红黑树,以提高元素搜索速度。
|
||||
关于 HashMap 实现解析参考 [这里](../../3%20HashTable/HashMap%20in%20Java.md)
|
||||
* 广泛应用Linux 的进程管理、内存管理,设备驱动及虚拟内存跟踪
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# 动态规划DP
|
||||
|
||||
动态规划(英语:Dynamic programming,简称 DP),通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
|
||||
|
||||
## 动态规划DP
|
||||
|
||||
复杂问题不能分解成几个子问题,而分解成一系列子问题;
|
||||
|
||||
DP通常基于一个递推公式及一个(或多个)初始状态,当前子问题解由上一次子问题解推出。
|
||||
复杂问题不能分解成几个子问题,而分解成一系列子问题;DP通常基于一个递推公式及一个(或多个)初始状态,当前子问题解由上一次子问题解推出。
|
||||
|
||||
状态
|
||||
状态转移方程
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
|
||||
## 回溯法
|
||||
# 回溯法
|
||||
|
||||
也叫 `试探法`。 是一种选优搜索法,按照选优条件搜索,当搜索到某一步,发现原先选择并不优或达不到目标,就退回重新选择。
|
||||
|
||||
回溯算法其实就是我们常说的 DFS 算法,本质上就是一种暴力穷举算法。
|
||||
|
||||
一般步骤
|
||||
|
||||
@@ -14,11 +13,20 @@
|
||||
回溯法求解问题时,一般是一边建树,一边遍历该树;且采用非递归方法。
|
||||
|
||||
|
||||
#### 八皇后问题
|
||||
## 全排列问题
|
||||
|
||||
> n 个不重复的数,全排列共有 n! 个
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 八皇后问题
|
||||
|
||||
8x8的国际象棋棋盘上放置8个皇后,使得任何一个皇后都无法直接吃掉其他的皇后。任意2个皇后都不能处于同一个 横线,纵线,斜线上。
|
||||
|
||||
分析
|
||||
|
||||
1. 任意2个皇后不能同一行,也就是每个皇后占据一行,通用的,每个皇后也要占据一列
|
||||
2. 一个斜线上也只有一个皇后
|
||||
|
||||
@@ -26,6 +34,7 @@
|
||||
|
||||
#### 其它案例
|
||||
|
||||
迷宫问题
|
||||
* 迷宫问题
|
||||
* 全排列
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
|
||||
## 递归
|
||||
# 递归
|
||||
|
||||
递归是一种设计和描述算法的有力工具。 递归算法执行过程分 `递推` 和 `回归` 两个阶段
|
||||
|
||||
@@ -11,20 +9,65 @@
|
||||
2) 确定边界条件
|
||||
|
||||
|
||||
#### 斐波那契数列
|
||||
## 斐波那契数列
|
||||
|
||||
fib(n)=fib(n-1)+fib(n-2)
|
||||
|
||||
|
||||
递归实现
|
||||
```
|
||||
#### 递归实现
|
||||
|
||||
```
|
||||
|
||||
非递归实现
|
||||
int fib(int N) {
|
||||
if (N == 1 || N == 2) return 1;
|
||||
return fib(N - 1) + fib(N - 2);
|
||||
}
|
||||
```
|
||||
|
||||
这个递归的算法效率非常差,存在大量重复计算。
|
||||
|
||||
|
||||
#### 非递归实现
|
||||
|
||||
我们可以有一个【缓存】,每次遇到一个子问题先去「缓存」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。
|
||||
|
||||
```
|
||||
public int fib(int n){
|
||||
if (n == 0) return 0;
|
||||
|
||||
//缓存
|
||||
int[] dp = new int[n + 1];
|
||||
|
||||
// base case
|
||||
dp[0] = 0; dp[1] = 1;
|
||||
|
||||
// 状态转移
|
||||
for (int i = 2; i <= n; i++) {
|
||||
dp[i] = dp[i - 1] + dp[i - 2];
|
||||
}
|
||||
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
说一个细节优化点,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1):
|
||||
|
||||
```
|
||||
int fib(int n) {
|
||||
if (n < 1) return 0;
|
||||
if (n == 2 || n == 1)
|
||||
return 1;
|
||||
int prev = 1, curr = 1;
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int sum = prev + curr;
|
||||
prev = curr;
|
||||
curr = sum;
|
||||
}
|
||||
return curr;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### 其它案例
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
* 链表中是否有环
|
||||
|
||||
|
||||
链表结点定义如下:
|
||||
struct ListNode
|
||||
{
|
||||
int m_nKey;
|
||||
ListNode* m_pNext;
|
||||
};
|
||||
|
||||
|
||||
|
||||
常用解题思路
|
||||
|
||||
@@ -19,16 +27,34 @@
|
||||
|
||||
|
||||
|
||||
## 输出链表中倒数第k个结点
|
||||
## 输出链表中倒数第k个节点
|
||||
|
||||
题目:输入一个单向链表,输出该链表中倒数第k个结点。链表的倒数第0个结点为链表的尾指针。
|
||||
|
||||
链表结点定义如下:
|
||||
struct ListNode
|
||||
{
|
||||
int m_nKey;
|
||||
ListNode* m_pNext;
|
||||
};
|
||||
思路一:2遍遍历,先遍历一遍链表算出 n 的值,然后再遍历链表计算第 n - k 个节点
|
||||
思路二:双指针,一个指针先走k步,然后 两个指针一起同步往前走,前面先走的指针到链表尾部吧,后面的指针刚好是第k个节点
|
||||
|
||||
|
||||
类似的题目还有
|
||||
|
||||
`删除第k个节点`
|
||||
`链表的中间结点` : 使用「快慢指针」的技巧 ,慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点。
|
||||
|
||||
|
||||
|
||||
## 给定单链表,检测是否有环。
|
||||
|
||||
```
|
||||
int isLinkCicle(Link *head);
|
||||
```
|
||||
|
||||
解题思路
|
||||
|
||||
1. 暴力
|
||||
2. 空间换时间,用一个HashMap
|
||||
3. 快慢指针: 使用两个指针p1,p2从链表头开始遍历,p1每次前进一步,p2每次前进两步。如果p2到达链表尾部,说明无环,否则p1、p2必然会在某个时刻相遇(p1==p2),从而检测到链表中有环。
|
||||
|
||||
|
||||
|
||||
|
||||
## 链表反转
|
||||
@@ -41,13 +67,6 @@ struct ListNode
|
||||
|
||||
输入一个链表的头结点,从尾到头反过来输出每个结点的值。链表结点定义如下:
|
||||
|
||||
```
|
||||
struct ListNode{
|
||||
int m_nKey;
|
||||
ListNode* m_pNext;
|
||||
};
|
||||
```
|
||||
|
||||
思路一: 辅助栈,需要一个栈空间
|
||||
思路二: 反转链表,然后遍历
|
||||
思路三: 递归实现,将printf语句放在递归调用后面。果然妙极。。
|
||||
|
||||
@@ -25,19 +25,6 @@ Node * sort_link_list_increasing_order (Node *pheader):
|
||||
|
||||
|
||||
|
||||
## 给定单链表,检测是否有环。
|
||||
|
||||
```
|
||||
int isLinkCicle(Link *head);
|
||||
```
|
||||
|
||||
解题思路
|
||||
|
||||
1. 暴力
|
||||
2. 空间换时间,用一个HashMap
|
||||
3. 快慢指针: 使用两个指针p1,p2从链表头开始遍历,p1每次前进一步,p2每次前进两步。如果p2到达链表尾部,说明无环,否则p1、p2必然会在某个时刻相遇(p1==p2),从而检测到链表中有环。
|
||||
|
||||
|
||||
|
||||
类似的题有:
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# 链表-2条
|
||||
|
||||
|
||||
## 找出两个链表的第一个公共结点
|
||||
|
||||
题目:两个单向链表,找出它们的第一个公共结点。链表的结点定义为:
|
||||
链表的结点定义为:
|
||||
```
|
||||
struct ListNode
|
||||
{
|
||||
@@ -12,13 +9,39 @@ struct ListNode
|
||||
};
|
||||
```
|
||||
|
||||
分析:第一个公共节点,也就是2个链表中的节点的m_pNext 指向的同一个节点。
|
||||
|
||||
## 找出两个链表的第一个公共结点
|
||||
|
||||
题目:两个单向链表,找出它们的第一个公共结点。
|
||||
|
||||
分析:第一个公共节点,也就是2个链表中的节点的m_pNext 指向的同一个节点。 2遍遍历方法:
|
||||
|
||||
1. 先遍历2个链表,得到各自的长度,和差sub
|
||||
2. 长链表先遍历sub个节点,然后2个节点一起遍历
|
||||
3. 第一次同时指向的同一个节点就是这个commonNode
|
||||
|
||||
|
||||
有没有可能一遍遍历就解决问题呢?
|
||||
|
||||
让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。
|
||||
空间复杂度为 O(1),时间复杂度为 O(N), 一遍遍历就搞定
|
||||
|
||||
```
|
||||
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
|
||||
// p1 指向 A 链表头结点,p2 指向 B 链表头结点
|
||||
ListNode p1 = headA, p2 = headB;
|
||||
while (p1 != p2) {
|
||||
// p1 走一步,如果走到 A 链表末尾,转到 B 链表
|
||||
if (p1 == null) p1 = headB;
|
||||
else p1 = p1.next;
|
||||
// p2 走一步,如果走到 B 链表末尾,转到 A 链表
|
||||
if (p2 == null) p2 = headA;
|
||||
else p2 = p2.next;
|
||||
}
|
||||
return p1;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 判断俩个链表是否相交
|
||||
@@ -32,6 +55,81 @@ struct ListNode
|
||||
|
||||
|
||||
|
||||
## 合并2条有序链表
|
||||
|
||||
while 循环每次比较 p1 和 p2 的大小,把较小的节点接到结果链表上
|
||||
|
||||
```
|
||||
ListNode mergeTwoLists(ListNode l1, ListNode l2) {
|
||||
// 虚拟头结点
|
||||
ListNode dummy = new ListNode(-1), p = dummy;
|
||||
ListNode p1 = l1, p2 = l2;
|
||||
|
||||
while (p1 != null && p2 != null) {
|
||||
// 比较 p1 和 p2 两个指针
|
||||
// 将值较小的的节点接到 p 指针
|
||||
if (p1.val > p2.val) {
|
||||
p.next = p2;
|
||||
p2 = p2.next;
|
||||
} else {
|
||||
p.next = p1;
|
||||
p1 = p1.next;
|
||||
}
|
||||
// p 指针不断前进
|
||||
p = p.next;
|
||||
}
|
||||
|
||||
if (p1 != null) {
|
||||
p.next = p1;
|
||||
}
|
||||
|
||||
if (p2 != null) {
|
||||
p.next = p2;
|
||||
}
|
||||
|
||||
return dummy.next;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 合并k个有序链表
|
||||
|
||||
|
||||
相比合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上?
|
||||
|
||||
就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点:
|
||||
|
||||
|
||||
```
|
||||
ListNode mergeKLists(ListNode[] lists) {
|
||||
if (lists.length == 0) return null;
|
||||
// 虚拟头结点
|
||||
ListNode dummy = new ListNode(-1);
|
||||
ListNode p = dummy;
|
||||
// 优先级队列,最小堆
|
||||
PriorityQueue<ListNode> pq = new PriorityQueue<>(
|
||||
lists.length, (a, b)->(a.val - b.val));
|
||||
// 将 k 个链表的头结点加入最小堆
|
||||
for (ListNode head : lists) {
|
||||
if (head != null)
|
||||
pq.add(head);
|
||||
}
|
||||
|
||||
while (!pq.isEmpty()) {
|
||||
// 获取最小节点,接到结果链表中
|
||||
ListNode node = pq.poll();
|
||||
p.next = node;
|
||||
if (node.next != null) {
|
||||
pq.add(node.next);
|
||||
}
|
||||
// p 指针不断前进
|
||||
p = p.next;
|
||||
}
|
||||
return dummy.next;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 输出两个非降序链表的并集
|
||||
|
||||
请修改append函数,利用这个函数实现:
|
||||
@@ -39,3 +137,11 @@ struct ListNode
|
||||
两个非降序链表的并集,1->2->3 和 2->3->5 并为 1->2->3->5 。另外只能输出结果,不能修改两个链表的数据。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,11 +32,53 @@
|
||||
|
||||
输入n,用最快的方法求该数列的第n项。
|
||||
|
||||
虽然fibonacci数列是`递归`的经典应用,但递归效率很差,会有很多重复的计算,复杂度是成指数递增的,我测试了下计算50的时候已经要300s了。
|
||||
思路一:`递归`,虽然fibonacci数列是`递归`的经典应用,但递归效率很差,会有很多重复的计算,复杂度是成指数递增的,我测试了下计算50的时候已经要300s了。
|
||||
|
||||
```
|
||||
int fib(int N) {
|
||||
if (N == 1 || N == 2) return 1;
|
||||
return fib(N - 1) + fib(N - 2);
|
||||
}
|
||||
```
|
||||
|
||||
思路二:从下往上计算,复杂度O(N),一个循环就搞定
|
||||
|
||||
思路三: logN
|
||||
```
|
||||
public int fib(int n){
|
||||
if (n == 0) return 0;
|
||||
|
||||
//缓存
|
||||
int[] dp = new int[n + 1];
|
||||
|
||||
// base case
|
||||
dp[0] = 0; dp[1] = 1;
|
||||
|
||||
// 状态转移
|
||||
for (int i = 2; i <= n; i++) {
|
||||
dp[i] = dp[i - 1] + dp[i - 2];
|
||||
}
|
||||
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
说一个细节优化点,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1):
|
||||
|
||||
```
|
||||
int fib(int n) {
|
||||
if (n < 1) return 0;
|
||||
if (n == 2 || n == 1)
|
||||
return 1;
|
||||
int prev = 1, curr = 1;
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int sum = prev + curr;
|
||||
prev = curr;
|
||||
curr = sum;
|
||||
}
|
||||
return curr;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user