C++ : 剑指offer(51-60)
文章目录
51、数组中的逆序对
题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述:(题目保证输入的数组中没有的相同的数字)
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2×10^5
注意取模就是求余数,对1000000007取余经常在一些大数问题中见到,这个数是大于10亿的最小质数,而int型一般就是42亿的范围;所以对此去余可以保证不管被除数是多大的数,最后的余数肯定小于1000000007,也就是int范围内;而且十亿零七很大,可以保证输出结果尽量多样,经得住测试序列测试;
class Solution {
public:
int InversePairs(vector<int>& data) {
if(data.empty()) return 0;
int size = data.size();
long int result = 0;
MergeSort(data,0,size-1,result);
return static_cast<int>(result%1000000007);
}
void MergeSort(vector<int>& data,int left,int right,long int& result){
if(right-left<=0) return;
int mid = (right+left)/2;
MergeSort(data,left,mid,result);
MergeSort(data,mid+1,right,result);
Merge(data,left,mid,right,result);
}
void Merge(vector<int>& data,int left,int mid,int right,long int& result){ // 001 321
int len1 = mid - left + 1;
int len2 = right - mid;
int l1[len1+1]; l1[0] = INT_MIN; // 归并中将最小哨兵添加到头部
int l2[len2+1]; l2[0] = INT_MIN;
for(int i=0;i<len1;++i){l1[i+1]=data[left+i];}
for(int i=0;i<len2;++i){l2[i+1]=data[mid+i+1];}
while(right>=left){ // 从后往前判断,如果l1[i]>l2[j],则共多出j个逆序对
data[right--] = l1[len1] > l2[len2] ? l1[len1] : l2[len2];
if(l1[len1]>l2[len2]){
result = result + (len2); // 归并中唯一核心计算逆序对的语句
--len1;
}else{
--len2;
}
}
}
};
思路:该问题可以直接遍历求解,但计算复杂度是O(n),一般情况下会要求更快捷的算法;该题的思路为,利用归并排序的思想,划分为多个子序列,在子序列的Merge过程中,Merge两个已排序的子序列(初始为1个元素的序列,默认已排序)的过程中,从子序列后面往前Merge,如果发现L1[i]>L2[j],则L1[i]就大于L2中从0到j的元素,多出j+1个逆序对;故该题可以利用改动归并排序完成;比较难想,可以灵活利用;
52、两个链表的第一个公共节点
题目描述
输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
if(pHead1==nullptr||pHead2==nullptr) return nullptr;
ListNode* pNode = pHead1;
int len1 = 0, len2 = 0;
while(pNode!=nullptr){
++len1;
pNode = pNode->next;
}
pNode = pHead2;
while(pNode!=nullptr){
++len2;
pNode = pNode->next;
}
int len = abs(len1-len2);
if(len1>len2) return findNode(pHead1,pHead2,len);
else return findNode(pHead2,pHead1,len);
}
ListNode* findNode(ListNode* pHead1, ListNode* pHead2,int len){
for(int i=0;i<len;++i){
pHead1 = pHead1->next;
}
while(pHead1!=pHead2){
pHead1 = pHead1->next;
pHead2 = pHead2->next;
}
return pHead1;
}
};
思路:问题并不复杂,有多种思路:第一种是借用两个栈存放两个链表的遍历节点,一快出栈,相同时即是公共节点;第二种是将尾结点链接到一个链表的头节点,问题变成简单版的求链表环的入口;第三种是分别遍历两个链表,计算长度,然后把长的那个先前进它们长度的差,然后一块前进,相等即公共节点;第二种方法和第三种方法本质上是一致的;
53、数字在排序数组中出现的次数
题目描述
统计一个数字在排序数组中出现的次数。
class Solution {
public:
int GetNumberOfK(vector<int> data ,int k){
if(data.empty()) return 0;
int left = getLeft(data,0,data.size(),k);
int right = getRight(data,0,data.size(),k);
int number = 0;
if(right>-1&&left>-1)
number = right - left + 1;
return number;
} // 寻找第一个元素
int getLeft(vector<int>& data,int left,int right,int k){
if(right-left<0) return -1;
int mid = (right+left)/2;
if(data[mid]==k){ // 如果当前节点等于k
if((mid>0&&data[mid-1]!=k)||mid==0){ // 如果其左侧的值不等于k或者左侧没值
return mid; // 返回k值的首元素
}
else{
right = mid - 1; // 不然继续寻找左侧区间
}
}
else if(data[mid]>k){ // 如果不等于,继续二分查找
right = mid - 1;
}
else{
left = mid + 1;
}
return getLeft(data,left,right,k);
} // 寻找最后一个元素
int getRight(vector<int>& data,int left,int right,int k){
if(right-left<0) return -1;
int mid = (right+left)/2;
if(data[mid]==k){
if((mid<data.size()-1&&data[mid+1]!=k)||mid==data.size()-1){
return mid;
}
else{
left = mid + 1;
}
}
else if(data[mid]>k){
right = mid - 1;
}
else{
left = mid + 1;
}
return getRight(data,left,right,k);
}
};
思路:该题可以直接遍历得到结果,但计算复杂度为O(n),既然是排序数组,则可以利用二分查找,查找到k值,然后左右遍历计数,这种算法在k重复数量很大时基本等价于遍历搜索;故可以利用两个二分函数分别查找第一个k值和最后一个k值,复杂度为O(2logn);练习了二分查找中的查找条件的灵活运用;
54、二叉搜索树的第k小节点
题目描述
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
class Solution {
public:
TreeNode* KthNode(TreeNode* pRoot, int k)
{
if(pRoot==nullptr||k<=0) return nullptr;
TreeNode* result = nullptr; // 保存返回节点
int num = 0; // 全局计数变量
traversal(pRoot,&result,&num,k);
return result;
} // 在中序遍历中,传递的result和num都必须是其原本类型的指针形式,防止函数中不改变原值
void traversal(TreeNode* pRoot,TreeNode** result,int* num,int k){
if(pRoot!=nullptr){
traversal(pRoot->left,result,num,k);
++(*num);
if(*num==k) *result = pRoot;
traversal(pRoot->right,result,num,k);
}
}
};
思路:直接利用二叉搜索树的中序遍历解决问题:左中右可以找到第k小节点,右中左可以找到第k大节点;注意保存计数和相应节点的参数传递到函数中时必须是原类型的指针形式,不然无法改变原值;
55、二叉树的深度
题目描述
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
class Solution {
public:
int TreeDepth(TreeNode* pRoot)
{
if(pRoot==nullptr) return 0;
int left = TreeDepth(pRoot->left); // 递归左子树的深度
int right = TreeDepth(pRoot->right); // 递归右子树的深度
return left > right ? left+1 : right+1; // 该节点深度为左右子树深度较大值+1;
}
};
思路:该方法非常巧妙;思路是以一个节点为根的子树的深度是其左右子树深度的较大值+1,递归过程从下往上,每次返回父节点的时候,返回子树深度较大值+1即可;
55(2)、判断是否为平衡二叉树AVL
题目描述
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
class Solution {
public:
bool IsBalanced_Solution(TreeNode* pRoot) {
// 不是AVL时,findDepth返回-1.该函数返回false;
// 是AVL树时,findDepth返回0~n,该函数返回true;
return findDepth(pRoot) != -1;
}
int findDepth(TreeNode* pRoot){
if(pRoot==nullptr) return 0; // 从空节点计数,为第0层;
int left = findDepth(pRoot->left); // 计算左子树深度
int right = findDepth(pRoot->right); // 计算右节点深度
// 如果左右子树不为AVL树时,或者左右子树的深度差大于1时,该树不是AVL树,返回-1
if(left==-1||right==-1||abs(left-right)>1) return -1;
// 如果当前节点属于AVL,则往父节点返回目前子树的深度+1;6
else return left > right ? left+1 : right+1 ;
}
};
思路:判断是否是AVL的条件是左右子树的深度差是否小于等于1;则要用到递归求树的深度的代码;但遍历所有节点并求深度会产生众多的重复计算子问题,故好办法是只遍历一次,从最下面一直遍历到最上面,采用后序遍历,从最下面开始计算深度01234、、、每次递归结束返回到父节点时都判断遍历过的左右子树是否满足AVL,满足则深度加1返回;当不满足时直接返回-1;
56、数组中只出现一次的数字
题目描述
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求计算复杂度O(n),空间复杂度o(1);
class Solution {
public:
void FindNumsAppearOnce(vector<int> data,int* num1,int* num2) {
if(data.size()<2||data.size()%2!=0) return; // 如果长度小于2或为奇数,错误
unsigned int YHnum = 0; // 定义异或结果
for(int i=0;i<data.size();++i){
YHnum = YHnum^data[i]; // 遍历数组,所有元素异或
}
int bitIndex = 0; // 计算异或结果从右边开始第几位是1
while(YHnum!=0){
if((YHnum&1)==1){
break;
}
++bitIndex;
YHnum = YHnum >> 1;
}
int YHval = 1; // 定义一个只有第几位是1的数字
while(bitIndex!=0){
YHval = YHval << 1;
--bitIndex;
}
unsigned int YHnum1 = 0, YHnum2 = 0; // 定义两个数组的异或结果
for(int i=0;i<data.size();++i){ // 遍历,分别异或
if((abs(data[i])&YHval)==YHval){
YHnum1 = YHnum1 ^ data[i];
}
else{
YHnum2 = YHnum2 ^ data[i];
}
}
*num1 = YHnum1;
*num2 = YHnum2;
}
};
思路:如果没有时空复杂度要求,则可以使用遍历、辅助索引等方法;但由于规定了严格的限制,所以需要找到数组的性质的规律。首先必须是偶数个元素,相同的两个元素异或起来结果为0,所以如果数组中只有一个数字出现了一次,其他是两次,则对数组进行异或,最后结果就是那个一次的数字;该题中假设所有异或结果为0100,则说明这两个只出现一次的数的第3位不同,于是根据第三位为1或零给数组分成两组,则相同的两个数肯定被分到了一组,两个不同的数肯定分到了两边,则分别异或,解决问题;
56(2)、数组中唯一只出现过一次的数字
题目描述:
一个数组只有一个数字出现了一次,其他的都出现了三次,求出这一个数;
class Myexception : exception{ // 异常类
public:
explicit Myexception(const string& s){
cout << s << endl;
}
};
int findone(vector<int>& data){
if(data.empty()){ // 如果为空,抛出异常
throw Myexception("Invalid Input");
}
int num[32] = {0}; // 定义数组统计int型的每一位之和
for(int i=0;i<data.size();++i){ // 遍历数组
int bitMask = 1;
for(int j=31;j>=0;--j){
if((data[i]&bitMask)==bitMask){
++num[j]; // 如果该位为1,数组相应数值+1
}
bitMask = bitMask << 1;
}
}
int result = 0; // 从数组中还原那个唯一的数
for(int i=0;i<32;++i){
result = result << 1;
result = result + num[i]%3; // 该位的值为数组该位值之和/3的余数
}
return result;
}
思路:该题可以利用排序、或创建哈希表来解决问题,但时空复杂度都不理想;该题同样可以利用位运算的思路:统计所有的数的二进制每一位的1的个数,利用32长度的数组保存,如果每个数都有三个,则每位之和肯定是3的倍数,而多出来的那个1就是只有一次的那个数对应位的1;
57、和为s的数字
题目描述
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,任意输出一对;
class Solution {
public:
vector<int> FindNumbersWithSum(vector<int> array,int sum) {
if(array.empty()||array.size()<2) return {};
int left = 0;
int right = FindNumtoS(array,0,array.size()-1,sum); // 二分查找右侧节点
while(right>left){ // 定义左右两端两个指针
long int longsum = array[left]+array[right];
if(longsum==sum){ // 如果两数和等与sum
return {array[left],array[right]};
}
else if(longsum<sum){ // 小于,left+1
++left;
}
else{
--right; // 大于,right-1
}
}
return {};
} // 二分查找小于等于sum的最后一个元素的下标
int FindNumtoS(vector<int>& array,int left, int right,int sum){
if(right<left) return -1;
int mid = (left+right)/2;
if(right>=left){ // 递归停止条件
if((mid<array.size()-1&&array[mid]<=sum&&array[mid+1]>sum)||mid==array.size()-1){
return mid; // 如果当前节点小于等于sum且下个节点大于sum,或者当前已经是最后的节点,就返回mid
}
else{ // 不然,直接二分
if(array[mid]<sum) left = mid + 1;
else right = mid - 1;
}
}
return FindNumtoS(array,left,right,sum);
}
};
思路:该题比较简单,设定两个一左一右的指针,如果两数大于sum,则左侧指针+1,如果两数之和小于sum,则右侧指针-1,直到和为sum或两个指针碰头;这道题需要明白这种方法不会漏掉某些情况,两个指针碰头就代表一定已经遍历完了所有情况;另外如果sum较小,还利用了二分查找找到了最右侧指针的位置;(另外注意,两个int之和可能大于int,所以建议设定和变量long int。
57(2)、和为s的连续正数数列
题目描述
找出所有和为S的连续正数序列,例如输入15,数列有12345、456、78(至少两个数)。
class Solution {
public:
vector<vector<int> > FindContinuousSequence(int sum) {
vector<vector<int> > result;
if(sum<3) return result;
int mysum = 3, left = 1, right = 2; // 定义当前和,左右指针
while(left<(sum+1)/2&&right>left){ // 如果左指针大于sum的一半则停止
if(mysum==sum){ // 如果当前序列符合条件
vector<int> oneresult;
for(int i=left;i<=right;++i){
oneresult.push_back(i); // 保存结果
}
result.push_back(oneresult);
++right; // 右指针+1
mysum = mysum + right - left; // 更新序列和
++left; // 左指针+1
}
if(mysum<sum){ // 如果当前序列和小于sum,则右指针+1
++right;
mysum = mysum + right;
}
else if(mysum>sum){ // 如果当前序列和大于sum,则左指针+1
mysum = mysum -left;
++left;
}
}
return result;
}
};
思路:该题与上一题查找两个和为s的数是一个思路,运用两个指针,指向1和2,然后如果sum小,就把right+1,如果sum大,就把left-1。一直到left等于sum的一半为止;
58、翻转字符串
题目描述:
翻转字符串的单词顺序,但不翻转每个单词中字母的顺序,如“student. a am I”应该是“I am a student.”。
class Solution {
public:
string ReverseSentence(string str) {
if(str.empty()) return str;
int left = 0, right = str.size()-1; // 定义左右指针
Inverse(str,left,right); // 翻转整个字符串
left = 0; right = 0;
while(right<str.size()){ // 依次翻转每个单词,直到右指针到头
if(str[right]==' '){ // 如果遇见空格,翻转空格前面的单词
Inverse(str,left,right-1);
left = right + 1;
}
if(right==str.size()-1){ // 如果是最后一个单词
Inverse(str,left,right); // 翻转这个单词
left = right + 1;
}
++right;
}
return str;
}
void Inverse(string& str, int left, int right){
while(right>left){
swap(str[left],str[right]);
++left; --right;
}
}
};
思路:这种题在本身可以在低复杂度下解决问题时,就尽量不要用辅助内存;先翻转整个字符串,再翻转每个单词中的字母顺序;
58(2)、左旋转字符串
题目描述
左旋转字符串就是汇编中的循环左移(ROL),例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。
class Solution {
public:
string LeftRotateString(string str, int n) {
if(str.empty()||n==0) return str;
int len = str.size();
n = n%len; // 排序n大于str.size()的情况
Inverse(str,0,len-1); // 先全部逆序
if(n>0){ // 如果n大于零,属于正常的循环左移
Inverse(str,0,len-n-1);
Inverse(str,len-n,len-1);
}
else{ // 如果n小于零,则变成循环右移
Inverse(str,0,-n-1);
Inverse(str,-n,len-1);
}
return str;
}
void Inverse(string& str,int left,int right){
while(right>left){ // 全部逆序
swap(str[left],str[right]);
++left;--right;
}
}
};
思路:巧妙利用上一题58翻转字符串中的两次逆序操作,先将str全部逆序,然后再根据循环左移的位数分两段重新逆序即可;这里要注意位移位数n大于0,小于0,绝对值大于字符串长度的各种情况;
59、滑动窗口的最大值
题目描述
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5};
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
vector<int> result; // 存储结果
deque<int> max; // 双端队列,存放索引,其front()为当前的最大值的索引
if(num.empty()||size==0||size>num.size()) return result;
for(int i=0;i<size;++i){ // 先将一个size装进双端队列max
while(!max.empty()&&num[i]>=num[max.back()]){
max.pop_back(); // 如果新加入的数大于之前插入的几个数
// 则说明之前的几个数肯定不会成为最大值了,pop掉
}
max.push_back(i); // 推入新加入的数
}result.push_back(num[max.front()]); // 录入当前第一个滑窗的最大值
for(int i=size;i<num.size();++i){ // 滑窗移动
if(i-size>=max.front()) max.pop_front(); // 如果front()已经移出窗口,则pop掉
while(!max.empty()&&num[i]>=num[max.back()]){ // 按照思路pop和push
max.pop_back();
}
max.push_back(i);
result.push_back(num[max.front()]); // 存储当前结果
}
return result;
}
};
思路:滑动窗口最大值问题,看起来简单但操作较为复杂,不太好想,有一定难度;设定一个双端队列,存放当前的最大值,为了方便判断有的节点已经在滑窗size范围以外,所以队列中存储下标;过程:现将第一个size处理到队列中,再依次滑动处理;每次push_back推入当前元素,如果当前元素大于等于之前推入的元素,说明之前的元素不可能再成为最大值,直接全部pop掉;如果下于,则说明当前元素也有可能成为最大值,直接push。然后判断一下队列front()端的值是否还在滑窗范围内,如果在,直接将这个值保存到最大值结果,不在就推出。。。
59(2)、队列的最大值
题目描述
实现一个队列,并实现max函数得到队列最大值,并且max、push_back、pop_front时间复杂度为O(1);
template<typename T>
class QueueMax{
private:
int Index; // 从0计数,每次push时+1
struct Data{ // 记录当前的序号和数值
T num;
T index;
};
deque<Data> data; // 数据双端队列
deque<Data> max; // 最大值双端队列(最大值永远在front端)
public:
QueueMax():Index(0){};
void push_back(T num){
while(!max.empty()&&num>=max.back().num){
max.pop_back(); // 如果当前的值大,之前的更小值pop
}
Data d = {num,Index}; // 添加当前节点
max.push_back(d);
data.push_back(d);
++Index; // 序列号增加
}
void pop_front(){
if(data.empty()) throw myexception("1");
if(data.front().index==max.front().index){ // 如果pop的数是当前序列的最大值
max.pop_front(); // 最大值队列也要pop
}
data.pop_front();
}
T getmax(){
if(data.empty()) throw myexception("2");
return max.front().num;
}
};
思路:利用上一题滑动窗口最大值的思路,创建双端队列deque保存当前队列中的最大值和后续可能出现的最大值,当前的最大值永远在front()端。每次push时,将deque尾部的比当前值小的数全部pop后推入。pop时,通过每个数的序号判断当前pop出的是不是序列的最大值,如果是则将最大值deque也pop。具体实现加上,思路难想,比较巧妙;
60、n个骰子的点数
题目描述:
把n个骰子扔在地上,所有的点数之和为s,求s所有可能的值出现的概率;
// 递归法
void CountProbability(int num, int left, int sum, vector<double>& result){
if(left==0){ // 如果当前统计完了最后一骰子
++result[sum-num]; // 当前加和S'的总次数+1
}
else{
for(int i=1;i<=6;++i){ // 当前投出1-6的不同结果时,再统计下一个骰子的情况
CountProbability(num,left-1,sum+i,result);
}
}
}
vector<double> findProbability(int num){
if(num<1) throw myexception("Invalid Input");
vector<double> result(5*num+1,0.0); // 初始化结果全零矩阵(共有5*num+1种S的值)
int left = num, sum = 0; // 定义剩余骰子个数、当前统计的骰子的点数之和
CountProbability(num,left,sum,result); // 进入递归,每个骰子逐个判断
int total_num = pow(6,num); // 所有骰子投出的情况总和
for(int i=0;i<result.size();++i){ // 遍历result
result[i] /= total_num; // 计算概率,result中所有和为s'的情况的次数总和肯定等于所有骰子投出的情况总和
}
return result;
}
思路:该题涉及到概率问题,较为难想;首先使用动态规划思想和概率思想来使用递归解决问题:n个骰子投出的结果之和在n-6n之间,共有5n+1种情况,而n个骰子投出的点数结果共有6的n次方种。用递归一个一个骰子地求出所有情况,当当前情况下的和为某一数时,对应数组的计数+1;最后所有计数之和肯定等于6的n次方。每种情况/6的n次方就是概率。该方法总体简洁明了,但递归的重复子问题依然众多,最好还是采用循环方法求解;
// 循环方法求解
vector<double> findProbability(int num){
vector<double> result;
if(num<=0) return result;
long long* Count[2];
Count[0] = new long long[6*num+1];
Count[1] = new long long[6*num+1];
for(int i=0;i<6*num+1;++i){
Count[0][i] = 0;
Count[1][i] = 0;
}
for(int i=1;i<=6;++i){
Count[0][i] = 1;
}
int flag = 1;
for(int k=2;k<=num;++k){ // 从第二个骰子开始计数
for(int i=0;i<k;++i){ // 将该数组的前一部分置零,防止错加
Count[flag][i] = 0;
}
for(int i=k;i<=6*k;++i){ // 第k个骰子的计数范围是k到6k
Count[flag][i] = 0; // 先将该数组置零,在累加
for(int j=1;j<=6;++j){ // fn=f(n-1)+...+f(n-6)
if(i-j>=0){ // 防止刚开始的几个骰子越界加和
Count[flag][i] += Count[1-flag][i-j];
}
}
}
flag = 1 - flag; // 每个骰子计数完毕后,交换两个数组
}
for(int i=0;i<6*num+1;++i){
cout << Count[1-flag][i] << " ";
}cout << endl;
// 循环完毕切换到最后一个统计的数组
double total_num = pow(6,num); // 全排列的种数,也是每种和的总情况数
for(int i=num;i<=6*num;++i){ // 遍历最后一个数组的计数范围
result.push_back(Count[1-flag][i]/total_num);
}
delete[] Count[0];
delete[] Count[1];
return result;
}
思路:一种计算更快但更难想的方法:一步一步统计和,统计方法是首先定义两个数组交替使用,存放第n个骰子投出点数之后当前各个和值的计数总数,下标表示和值,数值表示计和;例如第一个骰子,下标1-6分别计数为1;第二个骰子时,和为n的计数和等于第一个骰子的前六个计数和:f(n)=f(n-1)+…+f(n-6),依次统计到最后的筛子,就得到了所有和值的计数结果;之后计算概率和第一种方法相同;