diff --git a/算法/A类:基本算法/3.9 位运算算法.md b/算法/A类:基本算法/3.9 位运算算法.md index f1132c0b..22467a8e 100644 --- a/算法/A类:基本算法/3.9 位运算算法.md +++ b/算法/A类:基本算法/3.9 位运算算法.md @@ -26,10 +26,10 @@ n*2 | 等价于 左移一位 n << 1 ## 3 常见算法 ### 快速幂 +* 使用二进制方法,将幂转换成二进制。二进制每个位的权重就是可以递推计算,与二分法效果相同。 ``` double myPow(double x, int n) { - //使用二进制方法,将幂转换成二进制。二进制每个位的权重就是可以递推计算,与二分法效果相同。 long long N=n; if(N<0){ x=1/x; @@ -43,10 +43,23 @@ double myPow(double x, int n) { N= N>>1; } return result; + +``` +* 使用二分法、递归的方式求解快速幂。 +``` +//数学类题目。使用快速幂 +long long multi(long long x,int n){ + if(n == 0) return 1; + if(n == 1) return x; + long long half = multi(x, n / 2); + long long mod = multi(x, n % 2); + return (half * half * mod)%1000000007; +} ``` ### 快速乘法 +* 二进制的列竖式思想。 ``` int quickMulti(int A, int B) { int ans = 0; @@ -61,6 +74,8 @@ int quickMulti(int A, int B) { ### 快速加法 +* 利用按位与和按位异或运算。求解加法。 +* 循环加余法。 ``` int add(int a,int b){ cout<<(unsigned int)-1< reversePrint1(ListNode* head) { + stack s; + vector v; + ListNode * temp=head; + while(temp){ + s.push(temp->val); + temp=temp->next; + } + while(!s.empty()){ + v.push_back(s.top()); + s.pop(); + } + return v; + } +``` + +## 1.2 从尾到头打印链表内容——递归法 + +### 策略选择 + +* 数据结构:链表 +* 算法思想:枚举法 +* 利用递归后续遍历的特性(子节点在父节点之前除了) + +### 算法设计 + +1. 递推阶段: 每次传入 head.next ,以 head == None(即走过链表尾部节点)为递归终止条件,此时返回空列表 [] 。 +2. 回溯阶段: 利用 Python 语言特性,递归回溯时每次返回 当前 list + 当前节点值 [head.val] ,即可实现节点的倒序输出。 + +### 算法分析 + +* 时间复杂度O(n) +* 空闲复杂度O(n) + +### 算法实现 + +```C++ + vector reversePrint(ListNode* head) { + vector vec; + rp(head,vec); + return vec; + } + void rp(ListNode* head,vector&vec) { + if(head==nullptr)return ; + rp(head->next,vec); + vec.push_back(head->val); + return ; + } +``` + +## 2 反转链表 + +### 问题描述 + +* 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 +[链接](https://leetcode-cn.com/problems/reverse-linked-list-ii) + +## 2.1 反转链表——普通循环反转 +### 策略选择 + +* 循环 + +### 算法设计 + +* 在每次循环的时候。到达一个节点。 + * 记录本节点的下一个节点 + * 记录本节点的上一个几点 + * 反转本次节点指向上一个节点 + * 复制本层节点= 下一个节点。开始下一次循环。 + + +### 算法分析 + +* 时间复杂度O(n) +* 空间复杂度O(n) + +### 算法实现 +```C++ + ListNode* reverseBetween(ListNode* head, int left, int right) { + int i=1; + ListNode* node=head; + ListNode* last_node=nullptr; + ListNode* before_left=nullptr; + if(left==1){ + before_left=new ListNode(); + before_left->next=head; + } + ListNode* after_node; + while(true){ + after_node=node->next; + if(i==left-1){ + before_left=node; + } + if(i>left && i<=right){ + node->next=last_node; + } + if(i==right){ + before_left->next->next = after_node; + before_left->next=node; + break; + } + last_node=node; + node=after_node; + i++; + } + if(left==1)return before_left->next; + return head; + } +``` + +## 2.2 反转链表——头插法反转 + +### 策略选择 +* 循环 +* 插入反转 + +### 算法设计 + +* 每次循环的时候。到达本节点。将本节点的下一个节点插入到左节点前的下一个节点。从left开始 + * temp保留该节点的下一个几点。 + * 当前节点指向下一个节点的下一个节点(删除下一个节点) + * temp指向左前节点的下一个节点 + * 左前节点指向该节点(插入下一个几点) + +### 算法分析 + +* 时间复杂度O(n) +* 空间复杂度O(1) + +### 算法实现 + +``` +// 头插法 + ListNode* reverseBetween2(ListNode* head, int left, int right) { + ListNode* before_head = new ListNode(); + before_head->next=head; + ListNode* before_left; + ListNode* node=before_head; + ListNode* temp; + for(int i=0;i<=right;i++){ + if(i==left-1)before_left=node; + if(i>=left && inext; + node->next = temp->next; + temp->next=before_left->next; + before_left->next=temp; + } + else{ + node=node->next; + } + } + return before_head->next; + } +``` + +## 链表反转——递归法 + +### 策略选择 + +* 递归法 +* 等价于使用栈保存了节点的路径。不会在反转后回不到过去的节点 + +### 算法设计 + +* 递归 + * 当到右节点,使用全局变量记录右节点和右节点的下一个节点。 + * 当在左右之间时,直接使自身的下一个节点指向自己。 + * 当到达左节点时。与有节点和有节点的下一个反转。 + +* 递归的参数 +* 递归的返回值 +* 递归的执行 +* 递归的终止条件 +* 递归前的处理和递归后的处理。 + + +### 算法分析 + +* 时间复杂度O(n) +* 空间复杂度O(n) + +### 算法实现 + +```C++ +// 递归法,把它当做只有一侧的树。后续遍历。取得时候将节点路径放到栈里。回来的时候直接反转也能返回最初的节点。 + ListNode* reverseBetween(ListNode* head, int left, int right) { + ListNode* before_head = new ListNode(); + before_head->next=head; + dfs(before_head,left,right,0); + return before_head->next; + } + ListNode* right; + ListNode* right_next; + void dfs(ListNode* head,int left,int right,int i){ + if(i>right)return ; + // cout<next,left,right,i+1); + if(i>=left && inext->next=head;//下一个节点指向自己 + } + if(i==right){ + this->right=head; + this->right_next=head->next; + // cout<val<<"feji"<next->next=this->right_next; + head->next=this->right; + } + return ; + } +``` \ No newline at end of file diff --git a/算法/B类:数据结构算法/4.2 链表与双指针.md b/算法/B类:数据结构算法/4.2 链表与双指针.md new file mode 100644 index 00000000..9accb590 --- /dev/null +++ b/算法/B类:数据结构算法/4.2 链表与双指针.md @@ -0,0 +1,47 @@ +# 链表与双指针 + + +## 1 链表中的倒数第k个节点 + +### 问题描述 + +* 输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。 + +### 问题分析 + +* 典型的双指针问题 + +### 问题分类 + +* 数组与链表 +* 双指针问题 + +### 策略选择 + +* 蛮力法 + +### 算法流程 + +* 两个一样快的指针。相距k个距离。 +* 一个到达终点,另一个则为倒数第k个节点 + +### 算法分析 +* 时间复杂度O(n) +* 空间复杂度O(1) + +### 算法实现 + +```C++ + ListNode* getKthFromEnd(ListNode* head, int k) { + ListNode* first=head; + ListNode* second=head; + while(--k){ + second=second->next; + } + while(second->next){ + second = second->next; + first = first->next; + } + return first; + } +``` \ No newline at end of file diff --git a/算法/C类:问题类型算法/1.1 重复问题.md b/算法/C类:问题类型算法/1.1 重复问题.md index 00a252cd..e5feb2a6 100644 --- a/算法/C类:问题类型算法/1.1 重复问题.md +++ b/算法/C类:问题类型算法/1.1 重复问题.md @@ -1,4 +1,3 @@ - ## 1 重复数字问题 ### 问题描述 diff --git a/算法/C类:问题类型算法/2.1 反转链表.md b/算法/C类:问题类型算法/2.1 反转链表.md deleted file mode 100644 index 215c5411..00000000 --- a/算法/C类:问题类型算法/2.1 反转链表.md +++ /dev/null @@ -1,85 +0,0 @@ -# 反转链表 - - -## 1 从尾到头打印链表内容 - -### 问题描述 - -输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。 - -### 问题分类 -* 线性数据结构 -* 枚举法 -* 排序问题 - -### 问题分析 - -## 1.1 从尾到头打印链表内容——辅助栈 - -### 策略选择 - -* 数据结构:链表、栈、数组 -* 算法思想:枚举法 -* 可以利用栈的先进后出特性。 -### 算法设计 - - -1. 入栈: 遍历链表,将各节点值 push 入栈。(Python​ 使用 append() 方法,​Java​借助 LinkedList 的addLast()方法)。 -2. 出栈: 将各节点值 pop 出栈,存储于数组并返回。(Python​ 直接返回 stack 的倒序列表,Java ​新建一个数组,通过 popLast() 方法将各元素存入数组,实现倒序输出)。 -### 算法分析 -* 时间复杂度O(n) -* 空间复杂度O(n) - -### 算法实现 - -``` - vector reversePrint1(ListNode* head) { - stack s; - vector v; - ListNode * temp=head; - while(temp){ - s.push(temp->val); - temp=temp->next; - } - while(!s.empty()){ - v.push_back(s.top()); - s.pop(); - } - return v; - } -``` - -## 1.2 从尾到头打印链表内容——递归法 - -### 策略选择 - -* 数据结构:链表 -* 算法思想:枚举法 -* 利用递归后续遍历的特性(子节点在父节点之前除了) - -### 算法设计 - -1. 递推阶段: 每次传入 head.next ,以 head == None(即走过链表尾部节点)为递归终止条件,此时返回空列表 [] 。 -2. 回溯阶段: 利用 Python 语言特性,递归回溯时每次返回 当前 list + 当前节点值 [head.val] ,即可实现节点的倒序输出。 - -### 算法分析 - -* 时间复杂度O(n) -* 空闲复杂度O(n) - -### 算法实现 - -``` - vector reversePrint(ListNode* head) { - vector vec; - rp(head,vec); - return vec; - } - void rp(ListNode* head,vector&vec) { - if(head==nullptr)return ; - rp(head->next,vec); - vec.push_back(head->val); - return ; - } -``` - diff --git a/算法/C类:问题类型算法/4.1 正则表达式匹配问题.md b/算法/C类:问题类型算法/4.1 正则表达式匹配问题.md new file mode 100644 index 00000000..13dd9e58 --- /dev/null +++ b/算法/C类:问题类型算法/4.1 正则表达式匹配问题.md @@ -0,0 +1,139 @@ +# 正则表达式匹配问题 + +## 1 正则表达式匹配 + +### 问题描述 + +* 请实现一个函数用来匹配包含'\.'和'\*'的正则表达式。模式中的字符'\.'表示任意一个字符,而'\*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。 + +### 问题分析 + +* 可以使用动态规划。将字符串的规模增长的方向,作为动态变化的方向。状态变量表示当前的正则表达式,能够与之匹配。当规模发生变化时,保证字符串-1和正则表达式-1能够匹配。其实在这里感觉规模增长的方向,确实是有两个。但是状态变量的设置比较精巧。 + +### 问题分类 + +* 字符串匹配问题 +* 动态规划 +* 递归 + + +## 1.1 正则表达式匹配——动态规划 + +### 选择策略 +* 动态规划 + +### 算法设计 + +* 设s的长度为n ,pp 的长度为 m ;将 s 的第 i 个字符记为 s_i,p 的第 j个字符记为 p_j ,将 s 的前 i 个字符组成的子字符串记为s[:i] , 同理将 p 的前 j 个字符组成的子字符串记为 p[:j]p[:j] 。 + +* 因此,本题可转化为求s[:n] 是否能和p[:m] 匹配。 + +* 总体思路是从 s[:1]和 p[:1]是否能匹配开始判断,每轮添加一个字符并判断是否能匹配,直至添加完整个字符串s 和p 。展开来看,假设 s[:i]与 p[:j]可以匹配,那么下一状态有两种: + 1. 添加一个字符 $s_{i+1}$后是否能匹配? + 2. 添加字符 $p_{j+1}$后是否能匹配? + +* 本题的状态共有 m \times nm×n 种,应定义状态矩阵 dpdp ,dp[i][j]代表 s[:i]与 p[:j]是否可以匹配。做好状态定义,接下来就是根据 「普通字符」 , 「.」 , 「*」三种字符的功能定义,分析出动态规划的转移方程。 + +1. **状态定义**: 设动态规划矩阵 dp , dp[i][j] 代表字符串 s 的前 i 个字符和 p 的前 j 个字符能否匹配。 +2. **转移方程**: 需要注意,由于 dp[0][0] 代表的是空字符的状态, 因此 dp[i][j] 对应的添加字符是 s[i - 1] 和 p[j - 1] 。 + * 当 p[j - 1] = '*' 时, dp[i][j] 在当以下任一情况为 truetrue 时等于 truetrue : + 1. dp[i][j - 2]: 即将字符组合 p[j - 2] * 看作出现 0 次时,能否匹配. + 2. dp[i - 1][j] 且 s[i - 1] = p[j - 2]: 即让字符 p[j - 2] 多出现 1 次时,能否匹配; + 3. dp[i - 1][j] 且 p[j - 2] = '.': 即让字符 '.' 多出现 1 次时,能否匹配; + * 当 p[j - 1] != '*' 时, dp[i][j] 在当以下任一情况为 truetrue 时等于 truetrue : + 1. dp[i - 1][j - 1] 且 s[i - 1] = p[j - 1]: 即让字符 p[j - 1] 多出现一次时,能否匹配; + 2. dp[i - 1][j - 1] 且 p[j - 1] = '.': 即将字符 . 看作字符 s[i - 1] 时,能否匹配; +* **初始化**: 需要先初始化 dp 矩阵首行,以避免状态转移时索引越界。 +* **返回值** dp 矩阵右下角字符,代表字符串 s 和 p 能否匹配。 + +![](image/2021-03-18-20-39-26.png) + +### 算法分析 + +* 时间复杂度O(M*N) +* 空间复杂度O(M*N) + +### 算法实现 + +```C++ +class Solution { +public: + bool isMatch(string s, string p) { + int m = s.size() + 1, n = p.size() + 1; + vector> dp(m, vector(n, false)); + dp[0][0] = true; + for(int j = 2; j < n; j += 2) + dp[0][j] = dp[0][j - 2] && p[j - 1] == '*'; + for(int i = 1; i < m; i++) { + for(int j = 1; j < n; j++) { + dp[i][j] = p[j - 1] == '*' ? + dp[i][j - 2] || dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'): + dp[i - 1][j - 1] && (p[j - 1] == '.' || s[i - 1] == p[j - 1]); + } + } + return dp[m - 1][n - 1]; + } +}; +``` + +## 1.2 正则表达式匹配——递归 + + +### 选择策略 +* 递归 + +### 算法设计 + +* 递归的参数:字符串string、模式pattern、int i、int j分别表示当前字符串匹配到的位置,和模式行进到的位置。 +* 递归的返回值:返回值是当前分支是否成功。如果最后一个检测成果了,则返回true。如果最后没有检测成功,则返回false; +* 递归的执行: + * 如果下一个是*号。进行多分支递归讨论。 + 1. 匹配0次,i=i,j=j+2.进行下一轮递归。 + 2. 匹配1次,判断这一次是否成功,如果成功i=i+1,j=j + * 如果下一个不是*号。进行单分支递归。 + 1. 匹配正常字符或者.。匹配成功。i=i+1,j=j+1 + 2. 匹配不成功。return false; +* 递归的终止条件 + * i和j匹配都完成。return true; + * 其他情况return false +* 递归前和递归后的处理 + +### 算法分析 + +* 时间复杂度O(M+N) +* 空间复杂度O(M) + +### 算法实现 + +```C++ + bool isMatch(string s, string p) { + return isMatchR(s,p,0,0); + } + // 递归和循环混搭,果然很恶心。直接使用递归好了。 + // 递归的条件判断。正则表达式。 + // 算法设计的关键是,选择一个好的算法技术(递归或循环,两个是冗余的。) + // 然后设计好的算法路程。关键在于分类讨论的方式。如何合并类别。 + bool isMatchR(string &s,string &p,int i,int j){ + //递归终止的条件 + if(i>=s.size() && j>=p.size()){ + return true; + } + // cout< charint; + typedef unordered_map unmap; + bool isNumber(string s) { + vector states = { + unmap{charint(' ',0),charint('s',1),charint('d',2),charint('.',4)}, + unmap{charint('d',2),charint('.',4)}, + unmap{charint('d',2),charint('.',3),charint('e',5),charint(' ',8)}, + unmap{charint('d',3),charint('e',5),charint(' ',8)}, + unmap{charint('d',3)}, + unmap{charint('s',6),charint('d',7)}, + unmap{charint('d',7)}, + unmap{charint('d',7),charint(' ',8)}, + unmap{charint(' ',8)} + }; + int p = 0; + char t; + for(char c:s){ + if(c >= '0' && c <= '9') + t = 'd'; + else if(c == '+' || c == '-') + t = 's'; + else if(c == 'e' || c == 'E') + t = 'e'; + else if(c == '.' || c == ' ') + t = c; + else + t = '?'; + if(!states[p].count(t)) + return false; + p = (int) states[p][t]; + } + return p == 2 || p == 3 || p == 7 || p == 8; + } +``` +## 1.2 表示数值的字符串——反向分类讨论 + +### 算法设计 + +* 讨论所有可能出现的反例 + +* e/E 分割为指数和底数 + * 底数: + * 只能有一个+-号位于第一位 + * 只能有一个小数点 + * 指数 + * 只能有一个+-号位于第一位 + * 不能有小数点 + +### 算法分析 + +* 时间复杂度O(n) +* 空间复杂度O() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/算法/C类:问题类型算法/8.1 和积最大.md b/算法/C类:问题类型算法/8.1 和积最大.md new file mode 100644 index 00000000..dbf8dad7 --- /dev/null +++ b/算法/C类:问题类型算法/8.1 和积最大.md @@ -0,0 +1,81 @@ +# 和积最大 + + +## 1 剪绳子 + +### 问题描述 +* 给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m - 1] 。请问 k[0]*k[1]*...*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。 + +* 答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。 + +[链接](https://leetcode-cn.com/problems/jian-sheng-zi-ii-lcof) + +### 问题分析 + + +### 问题分类 + +* 贪心思想 +* 数值问题 +* 无数据结构 + +### 策略选择 + +* n尽可能多的包含因子3 + +### 算法设计 +1. 如果 n == 2,返回1,如果 n == 3,返回2,两个可以合并成n小于4的时候返回n - 1 +2. 如果 n == 4,返回4 +3. 如果 n > 4,分成尽可能多的长度为3的小段,每次循环长度n减去3,乘积res乘以3;最后返回时乘以小于等于4的最后一小段;每次乘法操作后记得取余就行 +4. 以上2和3可以合并 +### 算法分析 +* 时间复杂度O(logn) +* 空间复杂度O(1) + +### 算法实现 +```C++ + int cuttingRope1(int n) { + //果然是数学问题 + // 这东西由3和2的组成。3越多值越大。也就是存在三种情况。 + // n能被3整除。则全为3 + // n被3整除余数为1,则减掉两个2后,全为3 + // n被3整除余2,减掉1个2后,全为3 + if(n==2){ + return 1; + } + if(n==3){ + return 2; + } + + if(n%3==0){ + return multi(3,n/3); + } + else if(n%3==1){ + return (4*multi(3,(n-4)/3))%1000000007; + } + else if(n%3==2){ + return (2*multi(3,(n-2)/3))%1000000007; + } + return 0; + } + //数学类题目。使用快速幂 + long long multi(long long x,int n){ + if(n == 0) return 1; + if(n == 1) return x; + long long half = multi(x, n / 2); + long long mod = multi(x, n % 2); + return (half * half * mod)%1000000007; + } + + int cuttingRope(int n) { + if(n < 4){ + return n - 1; + } + long res = 1; + while(n > 4){ + res = res * 3 % 1000000007; + n -= 3; + } + return (int) (res * n % 1000000007); + } +``` \ No newline at end of file diff --git a/算法/C类:问题类型算法/image/2021-03-18-20-39-26.png b/算法/C类:问题类型算法/image/2021-03-18-20-39-26.png new file mode 100644 index 00000000..0a71f6e0 Binary files /dev/null and b/算法/C类:问题类型算法/image/2021-03-18-20-39-26.png differ diff --git a/算法/C类:问题类型算法/image/2021-03-18-21-06-06.png b/算法/C类:问题类型算法/image/2021-03-18-21-06-06.png new file mode 100644 index 00000000..07666067 Binary files /dev/null and b/算法/C类:问题类型算法/image/2021-03-18-21-06-06.png differ