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;