技巧题/规律总结题
斐波拉契数列
题目描述
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。
n<=39
思路
方法一、简单的暴力遍历计算
方法二、矩阵快速幂
class Solution {
public:
int Fibonacci(int n) {
vector<int>dp(n+1,0);
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
数值的整数次方
题目描述
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
保证base和exponent不同时为0
思路
快速幂思想
指数可能为负数,转换成 1/base-exponent的形式进行求解
【联想 斐波拉契数列】斐波拉契数列可以使用矩阵的快速幂进行复杂度为logn的求解,公式如何推导,想起来的时候可以回忆一下。
class Solution {
public:
double Power(double base, int exponent) {
double res=1;
double tmp=base;
// 要考虑指数为负数的情况!
int reverse=0;
if(exponent<0){
exponent = -exponent;
reverse = 1;
}
while(exponent){
int flag = (exponent&1);
if(flag){
res*=tmp;
}
tmp*=tmp;
exponent = (exponent>>1);
}
if(reverse==1)return 1.0/res;
return res;
}
};
数组中出现次数超过一半的数字
题目描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
思路
摩尔投票法,详细见代码注释。
【注意】这里可能出现不存在出现次数超过一半的数字,因此最后还是要统计幸存下来的数它出现的次数是否超过一半。
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int> numbers) {
int curNum = numbers[0]; // 初始化 numbers[0] 为当前候选数
int tmp = 1; // 当前候选数默认有1票
for(int i=1;i<numbers.size();i++){
if(tmp==0){ // 当票数耗尽 说明出现新的候选人
curNum = numbers[i]; // 更新候选人
tmp++; // 其默认票数 tmp++ 为1票
}else{
if(numbers[i]==curNum){ // 遍历到的数与候选数相同
tmp++; // 候选数的票数就增加
}else{
tmp--; // 否则该候选数被抵消一票
}
}
}
// 如果存在个数超过数组长度一半的数的话 curNum即为所求众数
// 但是如果不存在个数超过数组长度一半的数 curNum只能说是经过 投票/被抵消 这一过程后幸存下来的数
// 所以要检查 幸存下来的curNum是否是众数 ---> 数量书否尝过数组长度一半
int cnt=0;
for(int i=0;i<numbers.size();i++){
if(curNum==numbers[i]){
cnt++;
}
}
if(cnt>numbers.size()/2)return curNum;
return 0;
}
};
数字序列中某一位的数字
题目描述
数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
请写一个函数,求任意第n位对应的数字。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof。
思路
#include<string.h>
using namespace std;
class Solution {
public:
int findNthDigit(int n) {
/*
找规律
1-9 10-99 100-999 ...
有9个数字 有90个2位数字 有900个三位数字 ...
有9位 有90*2位 有900*3位 ...
区间内偏移量 = n - 9 - 90*2 - 900* 3 -....
区间内的第几个数 = 区间内偏移量 / 位数
数内的第几位 = 区间内偏移量 % 位数 (0表示数的最后一位)
区间内的数 = 区间基数(如 100) + 区间内的第几个数
*/
int digits = 1;
long baseNum = 9;
long domain_left = n;
// 1.获得区间内偏移量
while(domain_left - baseNum*digits>0){
domain_left = domain_left - baseNum*digits;
digits+=1;
baseNum*=10;
}
if(digits==1)return n;
printf("%d\n",domain_left);
// 2.获得区间基数
baseNum /= 9;
printf("current bits %d base%d\n", digits, baseNum);
// 3.区间内具体的数
int resNum;
if(domain_left % digits){ // 这里要使用domain_left % digits来判断是否-1
resNum = baseNum + domain_left/digits; // 区间内偏移量%区间位数!=0的情况下 不用-1
}else{
resNum = baseNum + domain_left/digits -1;// 区间内偏移量%区间位数==0的情况下 需要1
}
printf("%d\n",resNum);
// 4.获得数内的具体第几位
string r = to_string(resNum);
if(domain_left % digits){
return r[domain_left % digits - 1]-'0';
}else{
return r[r.size()-1]-'0';
}
return 0;
}
};
整数中1出现的次数
题目描述
求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
思路
/*
1.当前位为 0 时,当前位为1的数字出现次数 等于 high*digit
例如2304 high=23 cur=0 low=4 digit=10 high*digit=23*10=(00~22)*(0~9)
例如2034 high=2 cur=0 low=34 digit=100 high*digit=2*100=(0~2)*(00~99)
2.当前位为 1 时,当前位为1的数字出现次数 等于 high*digit + low + 1
例如2314 high=23 cur=1 low=4 digit=10 high*digit + low + 1 = 23*10+4+1
=(0~22)*(0~9) + (1*digit~1*digit+low) + 1(2310)=(0010~2219) + (2311~2314) + (2310)
3.当前位为其他值时,当前位为1的数字出现次数 等于 high*digit + digit
例如2324 high=23 cur=2 low=4 high*digit + digit = 23*10+10=240
= (0010~2219) + (2310~2319)
*/
class Solution {
public:
int NumberOf1Between1AndN_Solution(int n)
{
/*
1.当前位为 0 时,当前位为1的数字出现次数 等于 high*digit
例如2304 high=23 cur=0 low=4 digit=10 high*digit=23*10=(00~22)*(0~9)
例如2034 high=2 cur=0 low=34 digit=100 high*digit=2*100=(0~2)*(00~99)
2.当前位为 1 时,当前位为1的数字出现次数 等于 high*digit + low + 1
例如2314 high=23 cur=1 low=4 digit=10 high*digit + low + 1 = 23*10+4+1
=(0~22)*(0~9) + (1*digit~1*digit+low) + 1(2310)=(0010~2219) + (2311~2314) + (2310)
3.当前位为其他值时,当前位为1的数字出现次数 等于 high*digit + digit
例如2324 high=23 cur=2 low=4 high*digit + digit = 23*10+10=240
= (0010~2210) + (2310~2319)
*/
int high=n/10;
int cur=n%10;
int low=0;
int digit=1;
int cnt=0;
while(low!=n){
if(cur==0){
cnt += high*digit;
}else if(cur==1){
cnt += high*digit + low + 1;
}else{
cnt += high*digit + digit;
}
low = cur*digit + low;
cur = high%10;
high = high/10;
digit = digit*10;
}
return cnt;
}
};
数组中数字出现的次数
题目描述
在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
题目来源:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof/
思路
图片来自LeetCode本题的题解:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof/solution/mian-shi-ti-56-ii-shu-zu-zhong-shu-zi-chu-xian-d-4/
/*
方法一、
位运算+有限状态自动机
整体思路:
【nums中每个数都展开成32位二进制,】
每一位上,1的个数=3n+1或3n; 0的个数=3m+1或3m
每一位上,1的数量对3取模 就可以判断 target 在这一位上是不是1
two one nums[i] new_two new_one
【有限状态自动机】: 0 0 ---0---> 0 0
这里直接达到对3取模 0 0 ---1---> 0 1
0 1 ---0---> 0 1
0 1 ---1---> 1 0
1 0 ---0---> 1 0
1 0 ---1---> 0 0
注: 每一位都是一样的 所以只考虑nums[i]其中一位即可
当two=0时
new_one = one^nums[i] & ~two
当two=1时
new_one = 0
整理可得: new_one = one^nums[i] & ~two
当完成one的更新之后,每个状态里面 one已经变成new_one 但是two还是two,
此时 对调每个状态里面 new_one 和 two 的位置,则等价于原状态转换图
可用类似上面的公式得到new_two = two^nums[i] & ~new_one
遍历完所有数字后,各二进制位都处于状态 00 和状态 01(取决于 “只出现一次的数字”
的各二进制位是 1 还是 0 ),
而此两状态是由 one 来记录的(此两状态下 twos 恒为 0 ),因此返回 ones 即可。
方法二、中心思想和方法一差不多
直接遍历数组,然后统计二级制32位上每位1出现的次数对3取余,这个余数一定要么是0,要么是1,所有的0和1就组成最后只出现一次的数
*/
class Solution {
public:
int singleNumber(vector<int>& nums) {
int two=0,one=0; // 这里一定要两个都赋值0 不能int two,one=0;
for(int i=0;i<nums.size();i++){
one = one^nums[i] & ~two;
two = two^nums[i] & ~one;
}
return one;
}
};
数组中只出现一次的数字
题目描述
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
思路
1)首先遍历一遍所有的数字,获得所有数字异或结果,则这个异或结果c一定是数组中只出现一次的那两个数A、B的异或结果,即 c = A xor B
2)然后根据c二进制表示中为1的某位,设为第k位,说明A在第k位上和B在第k为上肯定一个是0,一个是1,才能异或出1来。
3)之后根据第k位是否为1,将数组中的数字分成两类,则A B必定不属于同一类。将数组中的数字第k位为1的一类一起异或,第k位为0的一类一起异或,一类异或的结果就是A,另一类异或的结果就是B
class Solution {
public:
void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
// 找出num1 和 num2 二进制表示 从低位到高位 第一个不同的位
int xores=0;
for(int i=0;i<data.size();i++){
xores^=data[i];
}
int flag=1;
while((xores&flag)==0){
flag<<=1;
}
int resA=0;
int resB=0;
for(int i=0;i<data.size();i++){
if((data[i]&flag)!=0){
resA^=data[i];
}else{
resB^=data[i];
}
}
*num1 = resA;
*num2 = resB;
}
};
孩子们的游戏
题目描述:
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
如果没有小朋友,请返回-1
思路:
1.建模 f(n,m)表示当圆圈长度为n时,每m步删一个数,【最终保留下】【第f(n,m)个】元素
2.第一次一定是删除 第m%n号 元素 ==== > 变成一个长为n-1的串 其解为f(n-1,m)
f(n-1,m)最终保留下来的数字就是 f(n,m) 在删除 第m%n号 元素 重排后 的序列中 继续接着删 能保留下来的元素序号
f(n,m)第一次删除 第m%n号 元素: n1 n2 ... nm%n ... nn
重排序列 : ... nn n1 n2 ...
能保留下的为重排序列中第 x = f(n-1,m) 元素 ... nn n1 n2 ...
0 x
恢复原来序列 n1 n2 ... nm%n ... nn
----------->x 0------>
有 这个 x 为 原来序列中的【第】f(n,m) = (m%n+x)%n = (m%n+f(n-1,m))%n 个元素
当f(1,m)在长为1的序列中每m步删掉一个元素 保留一个最终元素 必定是保留【第】0个元素
class Solution {
public:
int lastRemaining(int n, int m) {
// 约瑟夫环问题
// 方法一:以上思路可以使用递归实现
/*
if(n==1)return 0;
return (m%n+lastRemaining(n-1, m))%n;
*/
// 方法二:递归会使用系统的栈 避免使用 从底层写起
int f=0;
for(int i=2;i<=n;i++){// 这里的i指示约瑟夫环的长度 === 即上面方法迭代的轮次(倒序)
// 这里是对i取mod不是对n 是要根据序列长度变化的 每个f对应的序列长度n都不同 这里是变化的i
f = ( m%i + f)%i; // 新的f(轮次更靠后的f) 等于 (m%i+f(n-1,m))%i
}
return f;
}
};
剪绳子I
题目描述:
长度为n的绳子,剪成若干段,每段长度全部乘起来,乘积最大是多少
思路:
方法一:数学推导
根据算数几何均值不等式,当每段长度相等的情况下,每段长度乘积最大;
假设每段长为x,则乘积 = x(n/x),n固定,则问题转换成求乘积y=x1/x的最大值,两边同时取对数,求导,解得当x=3时,乘积最大。
有如下结论:
1)当n正好是3的倍数的时候,最大乘积=3n/3;
2)当n%3=1的时候,假设最后剪成b段,前b-2段的积为Pb,则乘积=Pb×3×1或者乘积=Pb×2×2,后者更大;
3)当n%3=2的时候,最大乘积=3n/3×2;
方法二:动态规划
令f[i]表示,绳子长度为i时,剪成若干段,能得到的乘积最大值;
则f[i] = max{ f[i], j×f[i-j]},其中1<=j<i;
// 方法1.
class Solution {
public:
int cutRope(int number) {
int left = number%3;
if(left==0){
return pow(3, number/3);
}else if(left==1){
// 这里是 * 不是加....
return pow(3, number/3 -1) * 2 * 2;
}else{
return pow(3, number/3) * 2;
}
}
};
// 方法2.
class Solution {
public:
int cutRope(int number) {
if (number == 2) {
return 1;
}
else if (number == 3) {
return 2;
}
vector<int> f(number + 1, -1);
for (int i = 1; i <= 4; ++i) {
f[i] = i;
}
for (int i = 5; i <= number; ++i) {
for (int j = 1; j < i; ++j) {
f[i] = max(f[i], j * f[i - j]);
}
}
return f[number];
}
};
链表中倒数第K个节点
题目描述
输入一个链表,输出该链表中倒数第k个结点。
思路
方式1.遍历一遍链表,求得链表长度n,再遍历一次链表,输出第n-k+1个节点;
方式2.快慢指针:构造相距k个单位的N1,N2指针,当N2指针指向末尾的空指针的时候,N1就指向倒数第K个节点。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
// 方式1
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
ListNode* q = pListHead;
int cnt=0;
int len=0;
ListNode* r = pListHead;
while(r!=NULL){
len++;
r = r->next;
}
if(len<k)return NULL;
while(q!=NULL){
cnt++;
if(cnt==len-k+1){
return q;
}
q = q->next;
}
return NULL;
}
};
// 方式2
ListNode* method2(ListNode* pListHead, unsigned int k){
if(pListHead==NULL||k<=0)return NULL;
ListNode* slow = pListHead;
ListNode* fast = pListHead;
while(k--){
if(fast){
fast = fast->next;
}else{
return NULL; // 这里说明链表长度不足k个
}
}
while(fast){
fast = fast->next;
slow = slow->next;
}
return slow;
}
两个链表的第一个公共节点
题目描述
输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
思路
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
/*
链表1非公共部分长度为l1
链表2非公共部分长度为l2
两者公共部分长度为C
当两条链表相交
①l1=l2
在p1 p2第一次游走时便能相等。
②l1≠l2
需要在第二次游走才能相等
当两条链表不相交
①l1=l2
第一次游走最后便能同时为空,造成相等,退出循环,返回空
②l1≠l2
第二次游走最后才能同时为空,造成相等,退出循环,返回空
因此不能使用
if(p1->next!=NULL){
p1=p1->next;
}
应该使用
if(p1!=NULL){
p1=p1->next;
}
这样最后返回p1,若为空,就是不相交。
不为空,就是交点。
否则,不相交时,p1 p2无法同时为空造成相等退出循环。
*/
ListNode* p1 = pHead1;
ListNode* p2 = pHead2;
while(1){
if(p1==p2)return p1;
if(p1!=NULL){
p1 = p1->next;
}else{
p1 = pHead2;
}
if(p2!=NULL){
p2 = p2->next;
}else{
p2 = pHead1;
}
}
return p1;
}
};
链表中环的入口节点
题目描述:
链表中存在环,找出环的入口节点,
思路:
链表中如果存在环,一定形成一个横着的ρ字型。
方法一、使用map或者set进行hash,检查每个节点是否出现过,出现两次的就是入口节点。
方法二、快慢双指针,快指针每次向后移动两个单位,慢指针每次向后移动一个单位;快指针一定比满指针先进入换;当它们第一次相遇P,快指针回归链表头结点,并且接下来一次只向后移动一个单位。
设环顺时针上右下左四个点为ABCD,头结点为N。
由于一开始快指针速度是满指针速度的两倍,因此 PBCDAP = NAP,即PBCDA = NA。当快指针与慢指针相遇,快指针回到N点重新出发,快慢指针两者再次相遇,一定是在A点相遇。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
/*
列表中如果存在环一定长这样
* * * * * A * * *
* *
* *
* * * *
A为环的入口节点
*/
// 方法一、使用 无序集合 哈希
/*
unordered_set<ListNode*> sl;
while(pHead){
if(sl.find(pHead)==sl.end()){
//找到集合末尾都没找到 说明不在集合中
sl.insert(pHead); // 插入集合
pHead = pHead->next; // 移至下一个节点
}else{
// 在集合中找到了
return pHead;
}
}
// 不存在环时 不存在环的入口节点 返回空
return NULL;
*/
// 方法二、 双指针相遇法
// 让 fast 和 slow 从头出发 在环中相遇
ListNode* fast = pHead;
ListNode* slow = pHead;
while(fast!=NULL && fast->next!=NULL){
// 不可能一开始 fast 就等于 slow (相遇)
fast = fast->next->next; // 一次2步
slow = slow->next; // 一次1步
// 此处在环中相遇
if(fast==slow) break;
}
// 无法相遇 不存在环
if(fast==NULL || fast->next==NULL){
// 是因为fast走到结尾才退出上面循环
// 而不是因为在环中相遇才退出====>因此可知不存在环
return NULL;
}
// 相遇 让fast回到头,一次1步
fast = pHead;
while(fast!=slow){
fast = fast->next;
slow = slow->next;
}
return fast;
}
};
1.不用加减乘除做加法
题目描述:
求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
思路:
1.将num1和num2表示二进制的形式便于观察
2.使用 “异或运算” 求 num1和num2 每一位上【不带进位】的和
3.使用 “与运算” 求 num1和num2 每一位上【向 前一位】的进位
只有当前位是两个1相加,才会出现进位,因此用“与运算”
比如:从低位开始算起,num1的第1位是1,num2的第一位也是1,它们相加得到的进位是1,这个1是要加在第2位上面的 ,因此得到的进位需要左移1位
4.下一步要完成 不带进位的和 与 进位 的加法。当进位=0,不带进位的和就是结果,算法结束。
【注意】求进位的时候一定要转换成-无符号整型-
class Solution {
public:
int Add(int num1, int num2)
{
/*
思路:
1.将num1和num2表示二进制的形式便于观察
2.使用 “异或运算” 求 num1和num2 每一位上【不带进位】的和
3.使用 “与运算” 求 num1和num2 每一位上【向 前一位】的进位
只有当前位是两个1相加,才会出现进位,因此用“与运算”
比如:从低位开始算起,num1和num2第1位的进位是1,则这个1是要加在第2位上面的
因为 得到的进位需要左移1位
4.下一步要完成 不带进位的和 与 进位 的加法。当进位=0,不带进位的和就是结果,算法结束。
*/
while(num2!=0){
// 1.求进位 按位与 并 左移一位 【细节: 一定要强转化为 无符号整型 】
int carry = (unsigned int)(num1&num2)<<1;
// 2.求无进位和
num1 = num1^num2;
num2 = carry;
}
return num1;
}
};
求1+2+3+…+n
题目描述:
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
思路:
使用&&逻辑运算符的短路效应
class Solution {
public:
int Sum_Solution(int n) {
// && 运算符的短路效应 + 递归
bool x = n>1 && ( n += Sum_Solution(n-1));
// 当n==1的时候 直接返回1 而不执行&&右边的递归加
return n;
}
};