From adad6993cc8b2e2d326b583c0d99237afced07e9 Mon Sep 17 00:00:00 2001 From: ranwenjie Date: Mon, 13 Sep 2021 10:42:01 +0800 Subject: [PATCH] update string --- 4 Tree/9-红黑树 R-B tree/红黑树.md | 11 +- 8 Algorithms Analysis/动态规划.md | 8 +- 8 Algorithms Analysis/回溯法.md | 19 ++- 8 Algorithms Analysis/递归.md | 61 +++++++-- 9 Algorithms Job Interview/2 链表.md | 47 ++++--- 9 Algorithms Job Interview/2.1 链表-排序.md | 13 -- 9 Algorithms Job Interview/2.3 链表-2条.md | 116 +++++++++++++++++- 9 Algorithms Job Interview/5 数组数列问题.md | 46 ++++++- .../{7 树.md => 7.9 树.md} | 0 9 files changed, 267 insertions(+), 54 deletions(-) rename 9 Algorithms Job Interview/{7 树.md => 7.9 树.md} (100%) diff --git a/4 Tree/9-红黑树 R-B tree/红黑树.md b/4 Tree/9-红黑树 R-B tree/红黑树.md index c537fcd..f963509 100644 --- a/4 Tree/9-红黑树 R-B tree/红黑树.md +++ b/4 Tree/9-红黑树 R-B tree/红黑树.md @@ -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 的进程管理、内存管理,设备驱动及虚拟内存跟踪 + + + + diff --git a/8 Algorithms Analysis/动态规划.md b/8 Algorithms Analysis/动态规划.md index cd14e8d..9ea7dab 100644 --- a/8 Algorithms Analysis/动态规划.md +++ b/8 Algorithms Analysis/动态规划.md @@ -1,10 +1,8 @@ +# 动态规划DP +动态规划(英语:Dynamic programming,简称 DP),通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 -## 动态规划DP - -复杂问题不能分解成几个子问题,而分解成一系列子问题; - -DP通常基于一个递推公式及一个(或多个)初始状态,当前子问题解由上一次子问题解推出。 +复杂问题不能分解成几个子问题,而分解成一系列子问题;DP通常基于一个递推公式及一个(或多个)初始状态,当前子问题解由上一次子问题解推出。 状态 状态转移方程 diff --git a/8 Algorithms Analysis/回溯法.md b/8 Algorithms Analysis/回溯法.md index e0836c1..4ed4442 100644 --- a/8 Algorithms Analysis/回溯法.md +++ b/8 Algorithms Analysis/回溯法.md @@ -1,9 +1,8 @@ - - -## 回溯法 +# 回溯法 也叫 `试探法`。 是一种选优搜索法,按照选优条件搜索,当搜索到某一步,发现原先选择并不优或达不到目标,就退回重新选择。 +回溯算法其实就是我们常说的 DFS 算法,本质上就是一种暴力穷举算法。 一般步骤 @@ -14,11 +13,20 @@ 回溯法求解问题时,一般是一边建树,一边遍历该树;且采用非递归方法。 -#### 八皇后问题 +## 全排列问题 + +> n 个不重复的数,全排列共有 n! 个 + + + + + +## 八皇后问题 8x8的国际象棋棋盘上放置8个皇后,使得任何一个皇后都无法直接吃掉其他的皇后。任意2个皇后都不能处于同一个 横线,纵线,斜线上。 分析 + 1. 任意2个皇后不能同一行,也就是每个皇后占据一行,通用的,每个皇后也要占据一列 2. 一个斜线上也只有一个皇后 @@ -26,6 +34,7 @@ #### 其它案例 -迷宫问题 +* 迷宫问题 +* 全排列 diff --git a/8 Algorithms Analysis/递归.md b/8 Algorithms Analysis/递归.md index 3a11497..ba9a406 100644 --- a/8 Algorithms Analysis/递归.md +++ b/8 Algorithms Analysis/递归.md @@ -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; +} +``` + + + + #### 其它案例 diff --git a/9 Algorithms Job Interview/2 链表.md b/9 Algorithms Job Interview/2 链表.md index 407d3b4..711d606 100644 --- a/9 Algorithms Job Interview/2 链表.md +++ b/9 Algorithms Job Interview/2 链表.md @@ -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语句放在递归调用后面。果然妙极。。 diff --git a/9 Algorithms Job Interview/2.1 链表-排序.md b/9 Algorithms Job Interview/2.1 链表-排序.md index f0f4288..00283e2 100644 --- a/9 Algorithms Job Interview/2.1 链表-排序.md +++ b/9 Algorithms Job Interview/2.1 链表-排序.md @@ -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),从而检测到链表中有环。 - - 类似的题有: diff --git a/9 Algorithms Job Interview/2.3 链表-2条.md b/9 Algorithms Job Interview/2.3 链表-2条.md index b80c6a6..89d565a 100644 --- a/9 Algorithms Job Interview/2.3 链表-2条.md +++ b/9 Algorithms Job Interview/2.3 链表-2条.md @@ -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 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 。另外只能输出结果,不能修改两个链表的数据。 + + + + + + + + diff --git a/9 Algorithms Job Interview/5 数组数列问题.md b/9 Algorithms Job Interview/5 数组数列问题.md index c4ecd31..8e97df9 100644 --- a/9 Algorithms Job Interview/5 数组数列问题.md +++ b/9 Algorithms Job Interview/5 数组数列问题.md @@ -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; +} +``` + diff --git a/9 Algorithms Job Interview/7 树.md b/9 Algorithms Job Interview/7.9 树.md similarity index 100% rename from 9 Algorithms Job Interview/7 树.md rename to 9 Algorithms Job Interview/7.9 树.md