C++ : 剑指offer(61-71)

C++ : 剑指offer(61-71)


61、扑克牌顺子

题目描述:
一副扑克牌有54张,现有56张(多了一副大小王),其中A为1,2-10,JQK分别为11、12、13, 大小王用0表示,其可以代表任何数;给出五个数,判断其是否可以组成顺子(12345、05689,00059等等).

class Solution {
public:
    bool IsContinuous( vector<int> numbers ) {
        if(numbers.empty()||numbers.size()!=5) return false;
        sort(numbers.begin(),numbers.end()); // 先进行排序
        int left = 3, right = 4, sum = 0; // 设定右侧两个指针和相邻元素差和的累计
        while(numbers[left]!=0&&left>=0){ // 从右往左统计,直到遍历完毕或遇到0
            if(numbers[right]-numbers[left]==0) return false; // 如果有重复数字则不是顺子
            sum = sum + (numbers[right]-numbers[left]-1); // 累加相邻数字之差-1
            --right; --left; // 如果全部是相邻的,则sum应该为0
        }
        if(left<0){ // 如果数组中没有0
            if(sum==0) return true; // sum为0,说明数组一直连续
            else return false;
        }else{ // 如果left>=0,说明数组中有0
            if(sum<=left+1) return true; // 零的个数是否>=数组中的差值,即用0补全是否够用
            else return false; 
        }
    }
};

思路:该题首先要将五个数字排序,方便分析。然后分有0和没0两种情况,没0时,从右往左两个两个相减都为1就可以;有0时,从右往左两两之差-1的和就是中间缺的数的个数,只要0的数量大于等于中间缺的个数,就是顺子,否则不是;同时应该注意数字重复的情况。


62、圆圈中最后剩下的数字

题目描述
让小朋友(n个)围成一个大圈。然后指定一个数m,让编号为0的小朋友开始报数1。每次喊到m-1的那个小朋友要出列,再从下一个小朋友开始,继续0…m-1报数到m出列,求出最后剩下的一个小朋友的编号;
如果没有小朋友,请返回-1

// 模拟链表法
int LastRemaining_Solution(int n, int m)
{
    if(n<=0||m<=0) return -1;
    list<int> list; // 定义一个辅助链表
    for(int i=0;i<n;++i){ // 先将链表循环赋值0~n-1
        list.push_back(i);
    }
    auto iter = list.begin(); // 定义迭代器
    while(n>1){ // 如果链表还有多个节点,继续循环
        for(int i=1;i<m;++i){
            ++iter;
            if(iter==list.end()){ // 如果当前循环到头了,则回到第一个节点
                iter = list.begin();
            }
        }
        iter = list.erase(iter); // 删除当前节点,返回下一个节点的迭代器
        if(iter==list.end()){ // 如果到头了,返回
            iter = list.begin();
        }
        --n; // n减少1
    }
    return *iter;
}

// 循环法
int LastRemaining_Solution(int n, int m){
    if(n<=0||m<=0) return -1;
    int last = 0;
    for(int i=2;i<=n;++i){
        last = (last+m)%i;  // f(n,m)=[f(n-1,m)+m]%n;
    }           // f(n,m) 表示n个节点间隔为m时的最终结果;
    return last;
}

思路:该方法使一个等长的list双向链表进行过程模拟,每次循环出列一人,如果循环到头,则返回头节点继续计数,直到最后。但该方法每隔m步使n减1,直到n==1,计算复杂度为O(mn),空间复杂度为O(n);更好的办法是找数学规律(较难):设参数为n,m时,最终结果记为f(n,m),然后由复杂的推导得出:f(n,m)=[f(n-1,m)+m]%n; 规律不细展开,规律可以稍加记忆;


63、股票的最大利润

题目描述:
将一只股票的售价按时间顺序存为一个数组,求出买卖一次该股票所获得的最大利润?(求数组中一个数值减去它左边的一个数值的最大差值),如{9,11,8,5,7,12,16,14}得到16-5=11;

int getmaxdiff(vector<int>& num){
    if(num.empty()) return 0;
    auto iter = num.begin() + 1;
    int min = num[0];  // 当前遍历序列之前序列中的最小值
    int maxdiff = 0; // 最大利润
    if(iter!=num.end())
        maxdiff = *iter-min; // 初始化为前两个元素之差
    while(iter!=num.end()){  // 遍历数组一遍
        // 如果当前节点减之前的最小值大于max,则更新max
        if(*iter-min>maxdiff) maxdiff = *iter-min;
        // 如果当前节点比之前的最小值都小,则更新最小值
        if(*iter<min) min = *iter;
        ++iter;
    }
    return maxdiff;
}

思路:该题思路不难,但想到需要一定的经验;首先除去暴力搜索法要追求遍历一遍数组的方法,首先定义遍历到当前节点之前的最小元素值min,当当前遍历节点与之前的最小值之差更大, 则更新maxdiff(最大利润),一直遍历到数组尾部;


64、求1+2+3+…+n

题目描述
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

// 利用构造函数调用
class Temp{
    static int num; // 声明静态变量
    static int sum; // 非const的静态变量必须在类外初始化
public:
    Temp(){ // 构造函数,每次构造对象,sum+n
        num = num + 1;
        sum = sum + num;
    }
    static int getsum(){ // 静态方法,独立于对象,可通过类直接调用
        return sum;
    }
    static void reset(){ // 静态方法,独立于对象,可通过类直接调用
        num = 0;
        sum = 0;
    }
};
int Temp::num = 0; // 类外初始化静态变量
int Temp::sum = 0;

class Solution {
public:
    int Sum_Solution(int n) {
        if(n<1) return 0;
        Temp::reset();
        // 一次创建n个对象,调用n次构造函数
        Temp* list = new Temp[n]; 
        delete[] list;
        list = nullptr;
        return Temp::getsum();
    }
};

思路:较为新奇,调用类的静态变量和构造函数,利用new一下子新建n个对象,static变量从1加到n;注意静态变量、静态方法的使用方式和特点;


// 短路递归法
class Solution {
public:
    int Sum_Solution(int n) {
        // 短路递归法,利用&&,若sum为0时则不执行后方的继续递归
        n && (n += Sum_Solution(n-1)); 
        return n;
    }
};

思路:最好的想法,只有一行代码,利用&&操作符的短路原理,代替了递归求解中的if语句;


65、不用加减乘除做加法

题目描述:题目描述
写一个函数,求两个整数之和,要求在函数体内不得使用加减乘除四则运算符号。

class Solution {
public:
    int Add(int num1, int num2){
        int a, b; // 定义异或结果(无进位相加)和&+<<1结果(只进位数)
        do{
            a = num1^num2; // 当前无进位相加
            b = (num1&num2)<<1; // 当前的进位情况
            num1 = a;
            num2 = b;
        }while(b!=0); // 直到没有进位时,当前异或结果就是结果
        return a;
    }
};

思路:非常巧妙的想法,利用两数相加过程中可以分解为先进行无进位相加结果,在把进位数值单独统计出来,最后相加两者得到答案的思路,将二进制加法也分解为这三步。规律是无进位相加部分可以用异或实现,进位部分可以用先与&在<<1实现,再将这两者相加;这两者相加的过程又分解为异或和&<<1,。。。循环,直到无进位值时,最后的异或结果就是答案;
特别注意:由于正负数的加减在计算机内部被统一为补码形式的相加运算,所以这里的位运算方法不需要处理正负数的问题,都符合同样的计算规律;


66、构建乘积数组

题目描述
给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2];)

class Solution {
public:
    vector<int> multiply(const vector<int>& A) {
        vector<int> result;
        int len = A.size();
        int left[len]; left[0] = A[0]; // 左乘积之和
        int right[len]; right[len-1] = A[len-1]; // 右乘积之和
        for(int i=1;i<len;++i){ // 左乘积赋值
            left[i] = left[i-1] * A[i];
        }
        for(int i=len-2;i>=0;--i){ // 右乘积赋值
            right[i] = right[i+1] * A[i];
        }
        for(int i=0;i<len;++i){ // 计算最后结果
            if(i==0) result.push_back(right[i+1]); // 如果是首元素
            else if(i==len-1) result.push_back(left[i-1]); // 如果是尾元素
            else result.push_back(left[i-1]*right[i+1]); // 其他
        }
        return result;
    }
};

思路:如果可以使用除法,则计算全部乘积,每个数除以A[i]即可;首先暴力法复杂度高,考虑构建辅助的结果数组,从最终要求的结果上分解问题,结果=左边乘积 乘 右边乘积,所以构建左边乘积的累加数组和右边乘积的累加数组,然后求总乘积即可;

(注:其实左右乘积数组只计算出一个即可,如只计算左乘积数组,然后从右侧开始往左遍历,顺便计算出右侧乘积给一个temp变量;也可以不使用额外辅助内存,直接在结果向量上保存左乘积的值,然后从右往左再依次更新;)


67、字符串转换为整数atoi

题目描述
实现一个atoi(字符串转换为整数)的函数;

class Solution {
public:
    int StrToInt(string str) {
        bool isnum = true; // 判断是否是合法输入
        if(str.empty()) isnum = false; // 字符串为空
        int PorN = 1, index = 0; // 正负标记+索引
        long int num = 0; // 长整形int,防止越界
        if(str[index]=='+'||str[index]=='-'){// 如果首字符为+-
            if(str[index]=='-') PorN = -1;
            ++index;
        } // 如果+-后面没有数值
        if(index>=str.size()||(str[index]<='0'&&str[index]>='9')) isnum = false; 
        while(index<str.size()&&str[index]!='.'){ // 如果遍历到末尾或者遇到小数点
            if(str[index]>='0'&&str[index]<='9'){ // 如果是正常数字
                num = num * 10 + (str[index]-'0');
                if(PorN*num>INT_MAX||PorN*num<INT_MIN) isnum = false;
            }
            else{ // 如果有不正常数字
                isnum = false; 
            }
            ++index;
        }
        if(isnum){ // 如果正常转化
            return static_cast<int>(num) * PorN;
        }
        else return 0; // 不然返回0
    }
};

思路:核心语句只有一行:num = num*10 + (*c-'0');
问题主要在于非法输入的处理,如输入指针为空、字符串为空、首字符为±,±之后没有数值或有非法字符,有小数点,有各种非法字符,超出int范围等等;考查思维的严谨性;


68、树中两个节点的最低公共祖先

题目描述:求树中两个节点的最低公共祖先;
1、树是二叉搜索树时,可以直接从根节点遍历,如果当前节点的值大于节点1小于节点2,则就是最低公共祖先;
2、树是普通树时(不是二叉树):
(1)树有指向父节点的指针,由两个节点往回遍历到根节点,转化为求链表的第一个公共节点;
(2)树没有指向父节点的指针,遍历这颗普通的树,顺便借助两个链表保存路径,遍历两次,保存两个链表,然后转换成求头部在一起,根部不在一起的两个链表的每没开的第一个节点;

// 该题程序省略

69、数组中重复的数字

题目描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

class Solution {
public: // duplication用来赋值重复的数,找到返回true
    bool duplicate(int numbers[], int length, int* duplication) {
        if(numbers==nullptr||length<2) return false;
        int index = 0; // 下标
        while(index<length){ // 从数组内循环
            if(numbers[index]==index) ++index; // 如果当前下标处的值等于下标
            else{ // 如果不等于,则把该值送到他对应的下标处
                int val = numbers[index]; // 该值
                if(val==numbers[val]){ // 如果该值对应的下标处已经有此值了
                    *duplication = val; // 重复,返回true
                    return true;
                }
                else{ // 不然则交换,把该值送到他对应的下标处
                    swap(numbers[index],numbers[val]);
                }
            }
        }
        return false;
    }
};

思路:一道有趣的题,创建辅助关联容器是比较low的做法;最优办法是充分利用所有的数在0-n-1之间,如果没有重复,则说明每个数x都可以对应放到下标x的位置;可以在原数组上进行操作,遍历数组,交换当前下标处的值到这个值对应的下标处,如果对应值的下标处已经有这个值了,则说明重复了;不然则把每个值放到属于他的下标处后,就没有重复的值;


70、字符流中第一个不重复的字符

题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
输出描述:
如果当前字符流没有存在出现一次的字符,返回#字符。

class Solution{
    int list[128] = {0}; // 简易哈希表,用来存放当前字符的数量
    queue<char> qu;    // 单身字符的队列
public:
  //Insert one char from stringstream
    void Insert(char ch){ 
        if(list[ch]==0){ // 如果该字符计数为0(第一次出现)
            qu.push(ch); // 入队列
        }
        ++list[ch]; // 计数加1
    }
  //return the first appearence once char in current stringstream
    char FirstAppearingOnce(){
        while(!qu.empty()){  // 出队,如果当前队头不是单身,则循环
            if(list[qu.front()]==1){ // 如果队头是单身
                return qu.front(); // 直接返回
            }
            else{
                qu.pop(); // 如果队头不是单身,则出队并循环
            }
        }
        return '#'; // 如队列为空,则没有单身,返回#
    }
};

思路:也是一道有趣的题,首先灵活利用队列容器(存放单身字符)和简易的字符哈希表(存放当前字符的计数),只将第一次出现的字符入队,然后返回第一个只出现了一次的字符时,检查当前队列头,如果计数为1,则直接返回;如果计数大于1,则该字符肯定不会再成为单身,将其出队,然后循环判断队头;


71、二叉树的下一个结点

题目描述
给定一个二叉树的其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

class Solution {
public:
    TreeLinkNode* GetNext(TreeLinkNode* pNode){
        // 为空,返回空
        if(pNode==nullptr) return nullptr;
        if(pNode->right!=nullptr){ // 有右子树
            // 返回右子树的最左节点
            return LeftOfRightChild(pNode->right);
        }
        else{ // 没右子树
            return FatherWhichFirstLeftChild(pNode);
        }
    } // 返回右子树的最左节点
    TreeLinkNode* LeftOfRightChild(TreeLinkNode* pNode){ 
        if(pNode==nullptr) return nullptr;
        while(pNode->left!=nullptr){
            pNode = pNode->left;
        }
        return pNode;
    } // 返回第一个为左子节点的父节点(或自身)的父节点,若无,返回nullptr
    TreeLinkNode* FatherWhichFirstLeftChild(TreeLinkNode* pNode){
        while(pNode->next!=nullptr){
            if(pNode->next->left==pNode) return pNode->next;
            pNode = pNode->next;
        }
        return nullptr;
    }
};

思路:没啥巧头,摸清规律,分清情况:分三种情况,首先是节点为空,则返回空;其次节点有右子节点,返回右子树的最左节点;若无右子节点,则返回第一个为左子节点的父节点(或自身)的父节点,若无,返回nullptr;


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值