剑指Offer刷题-技巧题/规律总结题

技巧题/规律总结题

斐波拉契数列

题目描述

大家都知道斐波那契数列,现在要求输入一个整数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~2210) + (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;
    }
};

猜你喜欢

转载自blog.csdn.net/qq_35903284/article/details/107771942