剑指offer|解析和答案(C++/Python) (五)上
参考剑指offer(第二版),这里做一个学习汇总,包括解析及代码。代码均在牛客网进行验证(摘自自己的牛客网笔记)。
整个剑指offer解析链接:
剑指offer|解析和答案(C++/Python) (一).
剑指offer|解析和答案(C++/Python) (二).
剑指offer|解析和答案(C++/Python) (三).
剑指offer|解析和答案(C++/Python) (四).
剑指offer|解析和答案(C++/Python) (五)上.
剑指offer|解析和答案(C++/Python) (五)下.
剑指offer|解析和答案(C++/Python) (六).
习题
面试中各项能力
1.数字在排序数组中出现的次数
2.0 ~ n-1中缺失的数字
3.数组中数值和下标相等的元素
4.二叉搜索树的第k个节点
5.二叉树的深度
6.平衡二叉树
7.数组中只出现一次的两个数字
8.数组中唯一只出现一次的数字
9.和为s的数字
10.和为s的连续正数序列
11.翻转单词顺序
12.左旋转字符串
13.滑动窗口的最大值
14.n个骰子的点数
1.数字在排序数组中出现的次数
统计一个数字在排序数组中出现的次数。例如:输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在这个数组中出现了4次,因此输出4。
思路:
使用二分法查找。
在二分法查找的基础上进行优化。
1.分别找出第一个数字k和最后一个数字k。使用二分法,判断中间的数字与k的比较,由于是排序数组,则很好判断第一个数字k和最后一个数字k的索引,通过索引计算重复次数。
2.先判断中间数字是否等于k,若等于k,则直接从中间开始搜索第一个k和最后一个k对应的索引,通过索引计算重复次数。
两个方法基本一致,时间复杂度均为**O(logn),**分别用C++和Python实现。
代码:
C++
class Solution {
public:
int GetNumberOfK(vector<int> data ,int k) {
if(data.size() == 0)//无符号整型不可能<0
return -1;
int number = 1;//重复的个数
int first = GetFirstK(data, k, 0, data.size() - 1);
int last = GetLastK(data, k, 0, data.size() - 1);
if(first > -1 && last > -1)//正确性判断
number = last - first + 1;
return number;
}
int GetFirstK(vector<int> data, int k, int start, int end){
//正确性判断
if(start > end)
return -1;
int middleIndex = (start + end)/2;
int middleData = data[middleIndex];
if(middleData == k){//中间那个数等于k
if((middleIndex > 0 && data[middleIndex - 1]!= k )|| middleIndex == 0)
//中间那个数就是第一个k 其实也是边界条件
return middleIndex;
else//第一个k还在前面
end = middleIndex - 1;
}else if(middleData > k)//第一个k在 前半段
end = middleIndex - 1;
else //第一个k在后半段
start = middleIndex + 1;
return GetFirstK(data, k, start, end);//进行递归
}
int GetLastK(vector<int> data, int k, int start, int end){
if(start > end)
return -1;
int middleIndex = (start + end)/2;
int middleData = data[middleIndex];
if(middleData == k){//中间那个数等于k
if((middleIndex < data.size() - 1 && data[middleIndex + 1] != k)
|| middleIndex == data.size() - 1){//中间那个数就是最后一个k
return middleIndex;//其实也是边界条件
}else
start = middleIndex + 1;//最后一个k 在后半段
}else if(middleData > k)
end = middleIndex - 1;//最后一个k 在前半段
else
start = middleIndex + 1;//最后一个k 在后半段
return GetLastK(data, k, start, end);//进行递归
}
};
Python
# -*- coding:utf-8 -*-
class Solution:
def GetNumberOfK(self, data, k):
# write code here
if len(data) == 0:
return 0
middle = len(data)//2 # //向下取整
#如果中间的数等于k
if data[middle] == k:
#开始计算k的开始和结束
start ,end = middle, middle
for i in range(middle -1, -1, -1):# 从中间往前面找
if data[i] == k:
start -= 1
for i in range(middle +1 , len(data), 1):#从中间往后面找
if data[i] == k:
end += 1
return end - start +1
elif data[middle] < k:#k全部在后面那个部分
#递归
return self.GetNumberOfK(data[middle + 1:], k)
else:#k全部在前面那个部分
#递归
return self.GetNumberOfK(data[0: middle], k)
2.0 ~ n-1中缺失的数字
一个长度为n - 1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0 - n-1之内。在范围0 - n-1之内的n个数字中有且只有一个数字不在该数组中,找出这个数字。
思路:
由于m不在数组中,那么m+1处在下标为m的位置,m+2处在下标为m+1的位置,以此类推。发现m正好是数组中第一个数值和下标不相等的下标,因此这个问题转换成:在排序数组中找出第一个值和下标不相等的元素。
借鉴上道题的二分法查找,每次通过比较数组中间的值进行递归。
当中间的数值不等于索引,但中间前面的数值等于索引,那么中间的索引值便是缺失的数值。
代码:
C++
class Solution {
public:
int GetMissingNumber(const int* numbers, int length){
if(numbers == nullptr || length <=0)
return -1;
int left = 0;
int right = length -1;
while(left <= right){
int middle = (left + right)>>1;//除以2
if(numbers[middle] != middle){//中间索引与对应的数字不相等时
//前面那个数字相等或者只有一个数字时
if(middle ==0 || numbers[middle -1] == middle -1)
return middle;//此时为边界条件 即缺失的数字
//否则缺失的数字在前半部分
right = middle -1;
}else
//缺失的数字在右半部分
left = middle + 1;
}
if(left == length)
return length
//无效的输入,比如数组不是按要求排序的
//或者有数字不在0~n-1范围之内的
return -1;
}
};
3.数组中数值和下标相等的元素
假设一个单调递增的数组里的每个元素都是整数并且是唯一的。请编程实现一个函数,找出数组中任意一个数值等于其下标的元素。例如:在数组{-3, -1, 1, 3, 5}中,数字3和它的下标相等。
思路:
参考前面两道题,使用二分法查找。
当下标和数值相等时,为递归的边界条件。
代码:
C++
class Solution {
public:
int GetNumberSameAsIndex(const int* numbers, int length){
if(numbers == nullptr || length <= 0){
return -1;
}
int left = 0;
int right = length - 1;
while(left <= right){
int middle = left + ((right - left)>>1);
if(numbers[middle] == middle)
return middle;//找到这个数字
if(numbers[middle] > middle)
right = middle -1;//这个数字在左边
else
left = middle + 1;//这个数字在右边
}
return -1;//每找到
}
};
4.二叉搜索树的第k个节点
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
思路:
参考图示:
该图的二叉搜索树的中序遍历为:{2, 3, 4, 5, 6, 7, 8}。因此只需要中序遍历一颗二叉搜索树,我们就很容易找到它第k个节点。
代码:
C++
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
TreeNode* KthNode(TreeNode* pRoot, int k)
{
if(pRoot == NULL || k == 0)
return NULL;
return KthNodeCore(pRoot, k);
}
//中序遍历
TreeNode* KthNodeCore(TreeNode* pRoot, int& k){
TreeNode* target = NULL;
if(pRoot->left != NULL)//遍历左子树
target = KthNodeCore(pRoot->left, k);
if(target == NULL){
if(k == 1)//表示已经到第k小的节点了
target = pRoot;
k--;
}
//target = NULL 表示没有找到
//开始遍历右子树
if(target == NULL && pRoot->right != NULL){
target = KthNodeCore(pRoot->right, k);
}
return target;
}
};
Python
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
# 返回对应节点TreeNode
def KthNode(self, pRoot, k):
# write code here
if pRoot == None or k <= 0:
return None;
#开始中序遍历
tree = []
self.KthNodeCore(pRoot, tree)
if len(tree) < k:
return None
return tree[k - 1]
def KthNodeCore(self, pRoot, tree):
if pRoot.left:
self.KthNodeCore(pRoot.left, tree)
tree.append(pRoot)#添加节点
if pRoot.right:
self.KthNodeCore(pRoot.right, tree)
5.二叉树的深度
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
思路:
参考图示:
该二叉树的深度为4,分别经过节点1,节点2,节点5,节点7。
1.如果一个树只有一个节点,那么深度为1.
2.如果一个根节点有子树,那么深度加1.
3.如果一个根节点即有左子树,也有右子树,那么该树的深度就是左、右子树深度的较大值加1.
代码:
C++
class Solution {
public:
int TreeDepth(TreeNode* pRoot)
{
//没有子节点
if(pRoot == NULL)
return 0;
//左子树的深度
int left = TreeDepth(pRoot->left);
//右子树的深度
int right = TreeDepth(pRoot->right);
//左子树比右子树深
if(left > right)
//当前节点深度是较深子树深度+1
return (left + 1);
else
return (right + 1);
}
};
Python
class Solution:
def TreeDepth(self, pRoot):
# write code here
if pRoot == None:
#没有节点 深度为0
return 0
left = self.TreeDepth(pRoot.left)
right = self.TreeDepth(pRoot.right)
if left > right:
return left + 1
else:
return right + 1
6.平衡二叉树
输入一棵二叉树,判断该二叉树是否是平衡二叉树。如果某二叉树中任意节点的左、右子树的深度相差不超过1,那么它就是一颗平衡二叉树。
思路:
1.重复遍历每个节点,计算深度并判断。可以参考上一题,计算每个节点的深度,但是这样会存在重复计算节点的深度。也就是,判断每个节点的时候,会计算左右子树的深度,然后在遍历左右子树进行判断时,部分节点会被重复计算。
参考代码:
C++:
class Solution {
public:
bool IsBalanced_Solution(TreeNode* pRoot) {
if(pRoot == NULL)
return true;
int left = TreeDepth(pRoot->left);
int right = TreeDepth(pRoot->right);
int diff = left - right;
if(diff>1 || diff<-1)
return false;
return IsBalanced_Solution(pRoot->left) && IsBalanced_Solution(pRoot->right);
}
int TreeDepth(TreeNode* pRoot)
{
//没有子节点
if(pRoot == NULL)
return 0;
//左子树的深度
int left = TreeDepth(pRoot->left);
//右子树的深度
int right = TreeDepth(pRoot->right);
//左子树比右子树深
if(left > right)
//当前节点深度是较深子树深度+1
return (left + 1);
else
return (right + 1);
}
};
2.每个节点只遍历一次的解法。 使用后续遍历二叉树的每个节点,那么在遍历到每一个节点之前就已经遍历了它的左、右子树。只要在遍历的时候记录它的深度,就可以一边遍历一边判断每个节点是否是平衡的。
代码:
C++:
class Solution {
public:
bool IsBalanced_Solution(TreeNode* pRoot) {
int depth = 0;
return IsBalanced(pRoot, &depth);
}
bool IsBalanced(TreeNode* pRoot, int* depth){
if(pRoot == NULL){
*depth = 0;
return true;
}
int left,right;
//后续遍历 先遍历左子树 再遍历右子树
if(IsBalanced(pRoot->left, &left)
&& IsBalanced(pRoot->right, &right)){
int diff = left - right;//做差
if(diff<=1 && diff>=-1){//满足平衡树条件
if(left > right)//当前节点深度是较深子树深度
*depth = 1 + left;
else
*depth = 1 + right;
return true;
}
}
//当 |diff| > 1时不满足平衡树条件
return false;
}
};
Python
参考:https://blog.csdn.net/qq_41359265/article/details/84224485
class Solution:
def IsBalanced_Solution(self, pRoot):
# write code here
return self.IsBalanced(pRoot) != -1
#通过后续遍历计算深度 如果为 -1 表示不是平衡树
def IsBalanced(self, pRoot):
if pRoot == None :
return 0
left = self.IsBalanced(pRoot.left)
#不是平衡树 直接返回 不再进一步判断计算深度
if left == -1:
return -1
right = self.IsBalanced(pRoot.right)
if right == -1:
return -1
#判断是否符合平衡树
if abs(left - right) > 1:
return -1
#返回当前层的深度
return max(left, right) + 1
7.数组中只出现一次的两个数字
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n), 空间复杂度是O(1)。
例如输入数组:{2,4,3,6,3,2,5,5},只有4和6只出现了一次,其余均出现了2次,所以输出4,6。
思路:
1.把问题拆开看,首先考虑,一组数组除了一个数字只出现一次,其余均出现了2次,请找出这个数字?这里利用异或运算的一个性质:任何一个数异或它自己都等于0。那么从头开始异或这组数字,那么由于只有一个数字只出现了1次,其他的数字均出现了2次,那么最后得到的数字便是只出现一次的数字。参考下列图示:
2.本题是有2个数字只出现了一次,那么考虑分组,把2个数字分别分到两个数组中,再利用上述的思路就可以分别求得对应的只出现1次的数字。 把数组{2,4,3,6,3,2,5,5}从头开始异或,得到0010 。0010表示数组中只出现一次的数字(4和6)在第21位上的数字是不同的,可以凭借这个条件进行分组成{2,3,6,3,2}和{4,5,5} 两组。如下图所示:
代码:
C++
class Solution {
public:
void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
if(data.size() < 2)//vector的长度不可能小于2
return ;
//设置每次异或的结果
int resultExclusiveOr = 0;
//遍历数组 依次异或
for(int i =0; i < data.size(); ++i){
resultExclusiveOr ^= data[i];
}
unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOr);
*num1 = *num2 = 0;
//再次遍历数组 分别异或求得只重复一次的数
for(int i = 0; i < data.size(); ++i){
if(IsBit1(data[i], indexOf1)){
*num1 ^= data[i];//数组1
}else{
*num2 ^= data[i];//数组2
}
}
}
//求得整数num 在二进制中最右边是1的位
unsigned int FindFirstBitIs1(int num){
unsigned int indexBit = 0;
while(((num & 1) == 0) && (indexBit < 8 * sizeof(int))){//防止indexBit溢出
num = num >> 1;
++indexBit;
}
return indexBit;
}
//判断num的二进制表达的第index位是否为1
bool IsBit1(int num, unsigned int indexBit){
num = num >> indexBit;
return (num & 1);
}
};
Python
# -*- coding:utf-8 -*-
class Solution:
# 返回[a,b] 其中ab是出现一次的两个数字
def FindNumsAppearOnce(self, array):
# write code here
length = len(array)
if length < 2:
return []
resultExclusiveOr = 0
for i in array:
resultExclusiveOr ^= i
indexBit = 0
while resultExclusiveOr & 1 == 0:
resultExclusiveOr >>= 1
indexBit += 1#位加1
#再次遍历
num1 = 0
num2 = 0
for i in array:
if self.IsBitIs1(i, indexBit):
num1 ^= i
else:
num2 ^= i
return [num1, num2]
def IsBitIs1(self, num, index):
num >>= index#移动到index位上
return (num & 1)#判断该位上是否为1
8.数组中唯一只出现一次的数字
在一个数组中只出现一次之外,其他数字都出现了三次,请找出那个只出现一次的数字。
思路:
可以参考上一道题的思路,但是不同的是这题不再采用异或这种方式,因为重复的数字是三次,使用异或无法抵消。不过仍然可以沿用位运算的思路:如果一个数字出现了三次,那么它的二进制表示的每一位(0或者1)都出现了3次。如果把所有出现三次的数字的二进制表示的每一位都分别加起来,那么每一位都能被3整除。
把数组的中的每个数字的二进制表示的每一位都加起来。如果某一位的和能被3整除,那么那个只出现一次的数字二进制表示中的那一位就是0;否则就是1.
这种解法的时间复杂度是O(n),空间复杂度是O(1)
代码:
C++
class Solution {
public:
int FindNumberAppearingOnce(int numbers[], int length){
if(numbers == NULL || length <= 0)
throw new std::exception("Invaild input");
int bitSum[32] = {0};//辅助数组 用来保存二进制的每一位
for(int i = 0; i <length; ++i){
int bitMask = 1;
for(int j = 31; j >= 0; --j){
//j=31 表示从右边(最低位)开始进行与运算
int bit = numbers[i] & bitMask;
if(bit != 0)
bitSum[j] += 1;
bitMask = bitMask<<1;//向高位移动移位
}
}
int result = 0;
for(int i = 0; i < 32; ++i){
result = result << 1;//首先计算的是高位 然后向高位移动(*2)
result += bitSum[i]%3;
}
return result;
}
};
9.和为s的数字
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
输出描述:
对应每个测试案例,输出两个数,小的先输出。
思路:
例如输入数组{1,2,4,7,11,15}和数字15。由于4+11=15,因此输出4和11。
更快的方法:定义两个指针,分别指向最后一个数字和第一个数字,并求和。如果求和数字大于s,则后面的指针向前移动。如果求和数字小于s,在前面的指针向后移动。如果相等,返回这两个数字。
代码:
C++
class Solution {
public:
vector<int> FindNumbersWithSum(vector<int> array,int sum) {
int start = 0;
int end = array.size() - 1;
int sumArr;
vector<int> found;
if(end <= 0)
return found;
while(start <= end){
sumArr = array[start] + array[end];
if(sumArr == sum){
found.push_back(array[start]);
found.push_back(array[end]);
break;//一定要加 不然超时 并且只需要得到一对即可
}else if(sumArr > sum){
--end;
}else{
++start;
}
}
return found;
}
};
Python
# -*- coding:utf-8 -*-
class Solution:
def FindNumbersWithSum(self, array, tsum):
# write code here
length = len(array)
found = []
if length <= 1:
return found
start = 0
end = length - 1
while start <= end:
sum = array[start] + array[end]
if sum == tsum:
found.append(array[start])
found.append(array[end])
break
elif sum < tsum:
start += 1
else:
end -= 1
return found
10.和为s的连续正数序列
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
输出描述:
输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
例如:
输入15,由于1+2+3+4+5 = 4+5+6 = 7+8,所以打印出3个连续序列1 - 5,4 - 6,7 - 8.
思路:
参考上道题。也分别使用small和big分别表示序列的最小值和最大值。
1.如果从small到big的序列的和大于s,则可以从序列中去掉较小的值,也就是增大small。
2.如果从small到big的序列的和小于s,则可以让序列包含更多的值,也就是增大big。
3.如果从small到big的序列的和等于s,则保存。
注意:
由于是连续正数序列,且至少包含2个数,那么一直增加small到(1+s)/2为止。
参考图示:
代码:
C++
class Solution {
public:
vector<vector<int> > FindContinuousSequence(int sum) {
vector<vector<int> > allSumIsS;//所有和为s的序列
vector<int> sumIsS;//单个和为s的序列
if(sum < 3)//至少有两个数 1,2,3,4......
return allSumIsS;
int small = 1;
int big = 2;
int middle = (1+sum)/2;//因为必须有两个数
int curSum = small + big;
while(small < middle){
if(curSum == sum){
//放入数据
for(int i = small; i <= big; ++i)
sumIsS.push_back(i);
allSumIsS.push_back(sumIsS);
sumIsS.clear();//清空
}
while(curSum > sum && small < middle){
curSum -= small;
++small;
if(curSum == sum){
//放入数据
for(int i = small; i <= big; ++i)
sumIsS.push_back(i);
allSumIsS.push_back(sumIsS);
sumIsS.clear();
}
}
++big;
curSum += big;
}
return allSumIsS;
}
};
Python
# -*- coding:utf-8 -*-
class Solution:
def FindContinuousSequence(self, tsum):
# write code here
sumIsS = []
allSumIsS = []
if tsum < 3:
return allSumIsS
small = 1
bif = 2
middle = (tsum + 1)//2
curSum = small + big
while small < middle:
if curSum = tsum:
for i in range(small, big + 1):
sumIsS.append(i)
allSumIsS.append(sumIsS)
sumIsS = []
while curSum > sum and small < middle:
curSum -= small
small -= 1
if curSum = tsum:
for i in range(small, big + 1):
sumIsS.append(i)
allSumIsS.append(sumIsS)
sumIsS = []
big += 1
curSum += big
return allSumIsS
11.翻转单词顺序
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
思路:
1.翻转句子中所有的字符。
比如,翻转“I am a student.”中所有字符为:“.tneduts a ma I”。
2.再翻转句子中每个单词中的字符。 这样就可以得到“student. a am I”.
代码:
C++
class Solution {
public:
string ReverseSentence(string str) {
if(str == "")
return "";
int length = str.length();
int endID = length - 1;
int startID = 0;
//翻转整个句子
Reverse(str, startID, endID);
//翻转句子中的每个单词
startID = 0;
endID = 0;
while(startID < length){
if(str[startID] == ' '){//遇到空格
++startID;
++endID;
}else if(str[endID] == ' '|| str[endID] == '\0'){//endID找到一个单词
//endID必须是前面那个有数字的数 不能是' '
Reverse(str, startID, endID - 1);
++endID;//开始下一个单词
startID = endID;
}else{
++endID;
}
cout<<"startID="<<startID<<endl;
cout<<"endID="<<endID<<endl;
cout<<str<<endl;
}
return str;
}
void Reverse(string &str, int startID, int endID){
if(str == "" || startID >= endID)
return ;
while(startID < endID){
char temp = str[startID];
str[startID] = str[endID];
str[endID] = temp;
++startID;
--endID;
}
}
};
Python
# -*- coding:utf-8 -*-
class Solution:
def ReverseSentence(self, s):
# write code here
#string 转list
listS = list(s)
length = len(listS)
if length == 0:
return ""
end = length - 1
start = 0
#翻转整个句子
self.Reverse(listS, start, end)
#开始翻转每个单词
start = 0
end = 0
while start < length:
#寻找单词的开头
if listS[start] == ' ':
start += 1
end += 1
elif listS[end] == ' ' :#寻找单词的末尾
self.Reverse(listS, start, end - 1)
end += 1
start = end
else:
end += 1
#列表转字符串
s = "".join(listS)
return s
def Reverse(self, s, start, end):
if s == "" or start >= end:
return
while start < end:
#交换顺序
s[start],s[end] = s[end], s[start]
start += 1
end -= 1
12.左旋转字符串
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
这道题其实就是字符串的左旋转操作。左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转功能。比如:输入字符串“abcdefg”和数字2,该函数返回左旋转2位的结果是“cdefgab”。
思路:
以“abcdefg”为例,我们可以把它分为2个部分。把前面要选择的字符分为第一个部分,剩下的字符为第二个部分。
1.先分别选择这两部分。于是就得到“bagfedc”。
2.接下来翻转整个字符串。可以得到结果“cdefgab”。
代码:
C++
class Solution {
public:
string LeftRotateString(string str, int n) {
if(str == "" || n < 0)
return "";
#翻转前面n个字符串
int startID = 0;
int endID = n - 1;
Reverse(str, startID, endID);
#翻转剩下的字符串
startID = n;
endID = str.length() - 1;
Reverse(str, startID, endID);
#翻转整个字符串
startID = 0;
endID = str.length() - 1;
Reverse(str, startID, endID);
}
void Reverse(string &str, int startID, int endID){
if(str == "" || startID >= endID)
return ;
while(startID < endID){
char temp = str[startID];
str[startID] = str[endID];
str[endID] = temp;
++startID;
--endID;
}
}
};
Python
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
class Solution:
def LeftRotateString(self, s, n):
# write code here
listS = list(s)#转列表
length = len(s)
if length == 0 or n < 0:
return ""
#翻转前面n个数字
startID = 0
endID = n -1
self.Reverse(listS, startID, endID)
#翻转后面的数字
startID = n
endID = length - 1
self.Reverse(listS, startID, endID)
#翻转整个字符串
startID = 0
endID = length - 1
self.Reverse(listS, startID, endID)
s = "".join(listS)
return s
def Reverse(self, s, start, end):
if s == "" or start >= end:
return
while start < end:
#交换顺序
s[start],s[end] = s[end], s[start]
start += 1
end -= 1
13.滑动窗口的最大值
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
思路:
1.传统方法。滑动窗口大学为k,需要在O(k)的时间里找到最大值。对于长度为n的数组,总复杂度是O(nk)。
2.使用辅助空间。剑指offer|解析和答案(C++/Python) (三)(https://blog.csdn.net/qq_24739717/article/details/100558101)中第4题(包含min函数的栈)中实现在O(1)时间内得到最小值的栈。同样,也可以在O(1)时间内得到栈的最大值。在剑指offer|解析和答案(C++/Python) (一)(https://blog.csdn.net/qq_24739717/article/details/100109632)中第7题(用两个栈实现队列)如何用2个栈实现一个队列。综合这两道题,如果把队列用2个栈实现,可以用O(1)时间得到栈中的最大值,那么也可以有O(1)时间内得到队列的最大值,因此总的时间复杂度就降到O(n)。
3.保存队列中最大值的下标。不把滑动窗口的每个值都存如队列,而是把有可能成为滑动窗口最大值的数值存入一个两端开口的队列。接着以输入数组{2,3,4,2,6,2,5,1}为例一步步分析。
参考图示:
a.队列中的下标依次存入。在存入数字之前,首先要判断队列里已有的数字是否小于待存入的数字。如果已有的数字小于待存入的数字,那么这些数字不可能是滑动窗口的最大值。因此它们将会被依次从队列尾部删除。
b.如果队列头部的数字已经从窗口里滑出,那么滑出的数字也需要从队列头部删除。
代码:
C++
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
vector<int> maxValues;
if(num.size() >= size && size >= 1){
//新建队列
deque<int> index;
//先输入一个滑动窗口的值
for(unsigned int i = 0; i < size; ++i){
//首先要判断队列里已有的数字是否小于待存入的数字
//如果小于待存入数字,则不可能是滑动窗口的最大值
//因此它们会被依次从队列中删除
while(!index.empty() && num[i] >= num[index.back()])
index.pop_back();
index.push_back(i);
}
for(unsigned int i = size; i < num.size(); ++i){
maxValues.push_back(num[index.front()]);
while(!index.empty() && num[i] >= num[index.back()])
index.pop_back();
if(!index.empty() && index.front() <= (int)(i - size))
index.pop_front();
index.push_back(i);
}
maxValues.push_back(num[index.front()]);
}
return maxValues;
}
};
Python
# -*- coding:utf-8 -*-
class Solution:
def maxInWindows(self, num, size):
# write code here
maxValues = []
if len(num) >= size and size >= 1:
#index用保存队列中的下标
index = []
#第一个滑动窗口
for i in range(size):
#当新放入是的数据比队列中所有的都大
#那么原队列中不可能存在最大值,需要清空
while len(index) > 0 and num[i] > num[index[-1]]:
index.pop()#删除队列的最后的元素
index.append(i)
#滑动窗口开始滑动
for i in range(size, len(num))
#先放入最大值
maxValues.append(num[index[0]])
#当新放入是的数据比队列中所有的都大
#那么原队列中不可能存在最大值,需要清空
while len(index) > 0 and num[i] > num[index[-1]]:
index.pop()#删除队列的最后的元素
#如果队列头部的数字已经从窗口滑出 也需要删除
while len(index) > 0 and index[0] <= (i - size):
index.pop(0)
14.n个骰子的点数
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
思路:
n个骰子的点数和的最小值为n,最大值为6n,所有点数的排列数为6n。要解决整个问题,需要统计出每个点数出现的次数,然后把每个点数出现的次数除以6n,就能求出每个点数出现的次数。
解法一:基于递归求骰子点数,时间效率不够高。
可以把n个骰子分为2堆:第一堆只有1个,另外一堆有n - 1个。单独一堆可能出现1 - 6的点数。需要计算1 - 6点数和剩下的n - 1个骰子来计算点数和。把剩下的n - 1仍然分为2堆:第一堆只有1个,另外一堆有n - 2个。依次递归,递归结束条件是最后只剩下一个骰子。
可以定义一个长度为6n - n + 1长度的数组,将和为s的点数出现的次数保存到第s - n个元素里。
代码:
C++
class Solution {
public:
int g_maxValue = 6;
void PrintProbability(int number){
if(number < 1)
return ;
int maxSum = number * g_maxValue;//6n
int *pProbabilities = new int[maxSum - number + 1];//6n - n + 1
for(int i = number; i <= maxSum; ++i)
pProbabilities[i - number] = 0;//初始化
//计算和为s出现的次数并保存在pProbabilities中
Probability(number, pProbabilities);
int total = pow((double)g_maxValue, number);
for(int i = number; i <= maxSum; ++i){
//计算概率
double ratio = (double)pProbabilities[i - number]/total;
printf("%d:%e\n",i, ratio);
}
delete[] pProbabilities;
}
void Probability(int number, int* pProbabilities){
for(int i = 1; i < g_maxValue; ++i)
//第一堆每个数字(1 - 6)都会出现一次 并在此基础下进行递归
ProbabilityCore(number, number, i, pProbabilities);
}
void ProbabilityCore(int original, int current, int sum, int* pProbabilities){
if(current == 1){
//递归结束条件就是只剩一个
pProbabilities[sum - original]++;// s - n
}else{
//把n个骰子分为2堆 一堆只有一个 另外一堆有n-1个
for(int i = 1; i <= g_maxValue; ++i){
//第一堆每个数字(1 - 6)都会出现一次 并在此基础下进行递归
ProbabilityCore(original, current - 1; i + sum; pProbabilities);
}
}
}
};
解法二:基于循环求骰子点数,时间性能好。
可以考虑用两个数组来存储骰子点数的每个总数出现的次数。
1.在一轮循环中,第一个数组中的第n个数字表示骰子和为n出现的次数。
2.在下一轮循环中,我们加上一个新骰子,此时和为n的骰子出现的次数应该等于上一轮循环骰子点数和为n - 1,n - 2, n - 3, n - 4, n - 5, n - 6个数字之和(骰子可能出现的点数为1 - 6)。
class Solution {
public:
int g_maxValue = 6;
void PrintProbability(int number){
if(number < 1)
return ;
int* pProbabilities[2];
//循环都是从1开始 比较好理解 所以数组长度为 6n + 1
pProbabilities[0] = new int[g_maxValue * number + 1];//6n + 1
pProbabilities[1] = new int[g_maxValue * number + 1];
//初始化
for(int i = 0; i < g_maxValue * number + 1; ++i){
pProbabilities[0][i] = 0;
pProbabilities[1][i] = 0;
}
int flag = 0;
for(int i = 1; i <= g_maxValue; ++i){
pProbabilities[flag][i] = 1;
}
for(int k = 2; k <= number; ++k){
//把另一个数组中的不可能出现的和置零
for(int i = 0; i < k; ++i)
pProbabilities[1 - flag][i] = 0;
for(int i = k; i <= g_maxValue * k; ++i){
pProbabilities[1 - flag][i] = 0;
//下一个循环中的和为n的次数 为上一次和为
// n-1,n-2,n-3,n-4,n-5,n-6的次次数之和 (骰子可能为1 - 6)
for(int j = 1; j <= i && j <= g_maxValue; ++j)
pProbabilities[1 - flag][i] += pProbabilities[flag][i - j];
}
flag = 1 - flag;
}
double total = pow((double)g_maxValue, number);
for(int i = number; i <= g_maxValue*number; ++i){
double ratio = (double)pProbabilities[flag][i]/total;
printf("%d:%e\n",i,ratio);
}
delete[] pProbabilities[0];
delete[] pProbabilities[1];
}
};