部分图片直接引用自代码随想录
基础数学
幂和对数
求解某个数x是不是4的n次幂
- x是以4为底n的对数,可以转化为2的对数方便程序计算。
- 相等判定中两个数为浮点数,如果要判定浮点数是否相等,需要使用相减小于某个非常小的数值的方法。
fabs(a - b) < 0.000000001
这样才能判定两个浮点数是否相等。
源码
bool isPowerOfFour(int n)
{
if(n <= 0)
{
return false;
}
int x = (int)(log2(n) / log2(4) + 1e-8);
return fabs(n - pow(4, x)) < 1e-8;
}
tips:
- 判定特殊情况,n=0.
- 换底公式,加上一个精度,避免精度损失。
- 浮点数相等判定。
leetcode题目:leetcode.cn/problems/po… 2的幂
神仙解法:
class Solution {
public:
bool isPowerOfTwo(int n) {
return (n > 0) && (n & -n) == n;
}
};
- 重点在于对位运算符的理解
- 解法1:&运算,同1则1。
return (n > 0) && (n & -n) == n; - 解释:2的幂次方在二进制下,只有1位是1,其余全是0。例如:8---00001000。负数的在计算机中二进制表示为补码(原码->正常二进制表示,原码按位取反(0-1,1-0),最后再+1。然后两者进行与操作,得到的肯定是原码中最后一个二进制的1。例如8&(-8)->00001000 & 11111000 得 00001000,即8。 建议自己动手算一下,按照这个流程来一遍,加深印象。
- 解法2:移位运算:把二进制数进行左右移位。左移1位,扩大2倍;右移1位,缩小2倍。
return (n>0) && (1<<30) % n == 0; - 解释:1<<30得到最大的2的整数次幂,对n取模如果等于0,说明n只有因子2。
矩阵
矩阵转置
首先明确在c++中可以使用一个n行m列的二维数组或是vector表示矩阵。
转置算法
int i, j;
for(i = 0, i < n, ++i)
{
for(j = 0, j < m, ++j)
{
tar[i][j] = 1 - src[i][m-1-j];
}
}
- src表示原矩阵,tar表示转置后的矩阵,先逆序,再取反,即用1去减,即可实现。
tips:
一定要记住vector的定义方式!!!!!!!!!!!!!
组合数
杨辉三角
int** generate(int numRows, int* returnSize, int** returnColumnSizes){
int i, j;
int **ret = (int **)malloc(numRows * sizeof(int *)); // (1)
*returnSize = numRows; // (2)
*returnColumnSizes = (int *)malloc(numRows * sizeof(int)); // (3)
for(i = 0; i < numRows; ++i) {
ret[i] = (int *)malloc( (i+1) * sizeof(int) ); // (4)
(*returnColumnSizes)[i] = i+1; // (5)
for(j = 0; j < i+1; ++j) {
if(j == 0 || j == i) {
ret[i][j] = 1; // (6)
}else {
ret[i][j] = ret[i-1][j] + ret[i-1][j-1]; // (7)
}
}
}
return ret;
}
cpp:
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> res;
if (numRows == 1)
{
vector<int> vec = {1};
res.push_back(vec);
return res;
}
if (numRows == 2)
{
vector<int> vec = {1};
res.push_back(vec);
vec.push_back(1);
res.push_back(vec);
return res;
}
vector<int> vec = {1};
vector<int> vec1 = {1,1};
res.push_back(vec);
res.push_back(vec1);
for (int i =2; i<numRows; ++i)
{
vector<int> temp(i+1,0);
temp[0] = 1;
for(int j = 1; j<i; ++j)
{
temp[j] = res[i-1][j-1] + res[i-1][j];
}
temp[i] = 1;
res.push_back(temp);
}
return res;
}
};
杨辉三角单行构造思路:
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> res;
res.push_back(1);
for(int i = 0; i < rowIndex; i++){
res.push_back((long)res[i]*(rowIndex-i)/(i+1));
}
return res;
}
};
计数方法
两数之和!!!!!彻底理解两数之和很重要!
const int MOD = 1e9 + 7;
class Solution {
using LL = long long;
public:
int countPairs(vector<int>& deliciousness) {
unordered_map<int,int> memo;
LL ans = 0;
for (int i = 0;i < deliciousness.size();++i){
for (int j = 0;j < 22;++j){
int target = pow(2,j);
if (target - deliciousness[i] < 0) continue;
if (memo.count(target - deliciousness[i])){
ans += memo[target - deliciousness[i]];
}
//两数之和的变式应用。
}
++memo[deliciousness[i]];
}
ans %= MOD;
return ans;
}
};
其中pow及其下面三行很重要。
tips:unordered_map底层是哈希表,map底层是树,查找元素使用哈希表更快实际速度是O(1),而树是O(n),此题使用map可能会超时。
日期计算
判断闰年
算法实现:
bool isLeapYear(int year) {
return (year % 4 == 0 && year % 100) || (year % 400 == 0);
}
天数计算:
bool isLeapYear(int y) {
return (y % 4 == 0 && y % 100 || y % 400 == 0);
}
int strToInt(char *str, int len) {
int i, sum = 0;
for(i = 0; i < len; ++i) {
sum = sum * 10 + (str[i] - '0');
}
return sum;
}
int dayOfYear(char * date){
int monthday[] = { 0, // (1) 存储第i月有多少天
31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31
};
int sumday[13], i;
int year, month, day;
year = strToInt(date + 0, 4); // (2)取字符串[0-3]的字符作为年
month = strToInt(date + 5, 2); // (3)取字符串[5-6]的字符作为月
day = strToInt(date + 8, 2); // (4)以上同理
monthday[2] = isLeapYear(year) ? 29 : 28; // (5)根据闰年还是平年重新填充二月的天数
sumday[0] = 0;
for (i = 1; i < month; ++i) { // (6)计算前面所有月份累加
sumday[i] = sumday[i - 1] + monthday[i];
}
return sumday[month - 1] + day; // (7)前面月份天数加当前月份天数
}
星期几的计算:
蔡勒公式:
-
c 是世纪数减一,也就是年份的前两位。
-
y 是年份的后两位。
-
m 是月份。m 的取值范围是 3 至 14,因为某年的 1、2 月要看作上一年的 13、14月,比如 2019 年的 1 月 1 日要看作 2018 年的 13 月 1 日来计算。
-
d 是该月第几天。
-
[] 代表对计算结果向下取整,只保留整数部分。
-
W = D%7 是结果,代表一周中第几天, 0 为周日。
由于历史原因以上公式仅适用于1582年10月15日及以后的情形(本题的数据范围满足此公式),如果要计算 1582.10.4 及之前的需要修改 D 的计算式,把最后的 d - 1 改成 d + 2即可。
此算法时空复杂度可以看成是O(1)。
基姆拉尔森计算公式:
-
y 是年份。
-
m 是月份。m 的取值范围是 3 至 14,因为某年的 1、2 月要看作上一年的 13、14月,比如 2019 年的 1 月 1 日要看作 2018 年的 13 月 1 日来计算。
-
d 是该月第几天。
-
[] 代表对计算结果向下取整,只保留整数部分。
-
W = D%7 是结果,代表一周中第几天, 0 为周日。
同样由于历史原因以上公式**仅适用于1582年10月15日及以后的情形。
代码参考:
class Solution {
public:
string dayOfTheWeek(int d, int m, int y) {
vector<string> weeks = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
if(m < 3) m += 12, --y;
int D = y + y/4 - y/100 + y/400 + 2*m + 3*(m+1)/5 + d + 1;
return weeks[D%7];
}
};
素数判定
素数判定
- 素数又称质数,首先满足条件大于等于2,并且除了他和本身,不能被其他任何自然数整除。
- 其他的数称为合数。
- 1既不是素数也不是合数。
- 2是唯一的偶素数。
判定算法:
对n做【2,n)范围内的余数判定,如果有至少一个数用n取余后为0,则表明n为合数;如果所有数都不能整除n,则n为素数,算法复杂度为O(n) 优化1:
- 如果一个数x是n的因子,那么n/x必然是一个整数,且n/x也一定是n的因子。其中,x和n/x一定有一个大小关系
不等式两边乘x
对两边开方
于是我们枚举时只需要枚举2到根号n范围内的数即可,这样一来,时间复杂度就变成了
优化2:
如果n是合数,那么他必然有一个小于等于根号n的素因子 ,只需要对根号n内的素数进行测试即可,需要预处理出根号n内的素数,假设该范围内素数的个数为s,那么复杂度降为O(s)。
KMP
应用:
主要用于字符串匹配问题
思想:
当出现字符串不完全匹配时,将能够匹配的部分记录下来,利用这些信息避免从头开始匹配。
前缀表:
前缀表用于回退,它记录了模式串与主串不匹配的时候,模式串应该从那里开始重新匹配。
前缀表将会记录下下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀
前缀指的是不包含最后一个字符的所有以第一个字符开头的连续子串
后缀指的是不包含第一个字符的所有以最后一个字符结尾的连续子串
使用KMP算法时常常会使用next数组来实现,next数组既可以就是前缀表,也可以是前缀表-1,以-1位置开始,这是两种不同的实现方式
时间复杂度分析
其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。
next数组的构造
void getNext(int* next, const string& s){
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
利用next数组进行模式串匹配
int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
return (i - t.size() + 1);
}
}
重复的子字符串
移动匹配
判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。
KMP匹配
在一个串中查找是否出现过另一个串,这是KMP的看家本领。那么寻找重复子串怎么也涉及到KMP算法了呢?
KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一个匹配过的位置继续匹配,靠的是有计算好的前缀表。 前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。
假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。
因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理)
所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。
next 数组记录的就是最长相同前后缀如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。
最长相等前后缀的长度为:next[len - 1] + 1。(这里的next数组是以统一减一的方式计算的,因此需要+1。
数组长度为:len。
如果len % (len - (next[len - 1] + 1)) == 0 ,则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串。
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
class Solution {
public:
void getNext (int* next, const string& s){
next[0] = -1;
int j = -1;
for(int i = 1;i < s.size(); i++){
while(j >= 0 && s[i] != s[j + 1]) {
j = next[j];
}
if(s[i] == s[j + 1]) {
j++;
}
next[i] = j;
}
}
bool repeatedSubstringPattern (string s) {
if (s.size() == 0) {
return false;
}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) {
return true;
}
return false;
}
};