剑指 Offer day3, day4
字符串和数组的操作。
剑指 Offer 05. 替换空格
剑指 Offer 05. 替换空格 - 力扣(Leetcode)
方法二:原地修改
在 C++ 语言中, string 被设计成「可变」的类型(参考资料),因此可以在不新建字符串的情况下实现原地修改。
由于需要将空格替换为 “%20” ,字符串的总字符数增加,因此需要扩展原字符串 s 的长度,计算公式为:新字符串长度 = 原字符串长度 + 2 * 空格个数 ,示例如下图所示。
算法流程:
- 初始化:空格数量 count ,字符串 s 的长度 len ;
- 统计空格数量:遍历 s ,遇空格则 count++ ;
- 修改 s 长度:添加完 “%20” 后的字符串长度应为 len + 2 * count ;
- 倒序遍历修改:i 指向原字符串尾部元素, j 指向新字符串尾部元素;当 i = j 时跳出(代表左方已没有空格,无需继续遍历);
当 s[i] 不为空格时:执行 s[j] = s[i] ;
当 s[i] 为空格时:将字符串闭区间 [j-2, j] 的元素修改为 “%20” ;由于修改了 3 个元素,因此需要 j -= 2 ; - 返回值:已修改的字符串 s ;
复杂度分析:
时间复杂度 O(N): 遍历统计、遍历修改皆使用 O(N)时间。
空间复杂度 O(1): 由于是原地扩展 s 长度,因此使用 O(1)额外空间。
class Solution {
public:
string replaceSpace(string s) {
int count = 0, len = s.size();
// 统计空格数量
for (char c : s) {
if (c == ' ') count++;
}
// 修改 s 长度
s.resize(len + 2 * count);
// 倒序遍历修改
for(int i = len - 1, j = s.size() - 1; i < j; i--, j--) {
if (s[i] != ' ')
s[j] = s[i];
else {
s[j - 2] = '%';
s[j - 1] = '2';
s[j] = '0';
j -= 2;
}
}
return s;
}
};
剑指 Offer 58 - II. 左旋转字符串
剑指 Offer 58 - II. 左旋转字符串 - 力扣(Leetcode)
由于 python 中 字符串是不可变对象,提供一个 c++ 实现,大概原理就是 进行三次翻转,代码如下:
class Solution {
public:
string reverse(string& s, int start, int end) {
while (start < end) {
swap(s[start++], s[end--]);
}
return s;
}
string reverseLeftWords(string s, int n) {
reverse(s, 0, n - 1);
reverse(s, n, s.size() - 1);
reverse(s, 0, s.size() - 1);
return s;
}
};
剑指 Offer 03. 数组中重复的数字
剑指 Offer 03. 数组中重复的数字 - 力扣(Leetcode)
原地交换的方法很有意思,哈希表就比较常规了
方法二:原地交换
题目说明尚未被充分使用,即 在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此说明含义:数组元素的 索引 和 值 是 一对多 的关系。 因此,可遍历数组并通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i]=i )。因而,就能通过索引映射对应的值,起到与字典等价的作用。
遍历中,第一次遇到数字 x 时,将其交换至索引 x 处;而当第二次遇到数字 x 时,一定有 nums[x]=x,此时即可得到一组重复数字。
算法流程:
- 遍历数组 nums ,设索引初始值为 i=0 :
- 若 nums[i]=i : 说明此数字已在对应索引位置,无需交换,因此跳过;
- 若 nums[nums[i]]=nums[i] : 代表索引 nums[i] 处和索引 i 处的元素值都为 nums[i] ,即找到一组重复值,返回此值 nums[i];
- 否则: 交换索引为 i 和 nums[i] 的元素值,将此数字交换至对应索引位置。
- 若遍历完毕尚未返回,则返回 −1。
复杂度分析:
时间复杂度 O(N) : 遍历数组使用 O(N) ,每轮遍历的判断和交换操作使用 O(1) 。
空间复杂度 O(1) : 使用常数复杂度的额外空间。
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int i = 0;
while(i < nums.size()) {
if(nums[i] == i) {
i++;
continue;
}
if(nums[nums[i]] == nums[i])
return nums[i];
swap(nums[i],nums[nums[i]]);
}
return -1;
}
};
剑指 Offer 53 - I. 在排序数组中查找数字 I
本来的想法是找到target了就直接遍历了,题解的方法是边界也要用二分的方法找,难度增加了。
注意边界条件。
解题思路:
排序数组中的搜索问题,首先想到 二分法 解决。
排序数组 nums中的所有数字 target 形成一个窗口,记窗口的 左 / 右边界 索引分别为 left 和 right ,分别对应窗口左边 / 右边的首个元素。
本题要求统计数字 target 的出现次数,可转化为:使用二分法分别找到 左边界 left 和 右边界 right ,易得数字 target 的数量为 right−left−1。
算法解析:
- 初始化: 左边界 i=0,右边界 j=len(nums)−1。
- 循环二分: 当闭区间 [i,j][i, j][i,j] 无元素时跳出;
a. 计算中点 m=(i+j)/2(向下取整);
b. 若 nums[m]<target,则 target 在闭区间 [m+1, j]中,因此执行 i=m+1;
c. 若 nums[m]>target,则 target 在闭区间 [i, m−1]中,因此执行 j=m−1;
d. 若 nums[m]=target,则右边界 right在闭区间 [m+1, j] 中;左边界 left在闭区间 [i, m−1] 中。因此分为以下两种情况:- 若查找 右边界 right,则执行 i=m+1;(跳出时 i 指向右边界)
- 若查找 左边界 left,则执行 j=m−1;(跳出时 j 指向左边界)
- 返回值: 应用两次二分,分别查找 right 和 left ,最终返回 right−left−1 即可。
效率优化:
以下优化基于:查找完右边界 right=i 后,则 nums[j] 指向最右边的 target(若存在)。
- 查找完右边界后,可用 nums[j]=j判断数组中是否包含 target,若不包含则直接提前返回 0 ,无需后续查找左边界。
- 查找完右边界后,左边界 left 一定在闭区间 [0, j] 中,因此直接从此区间开始二分查找即可。
复杂度分析:
时间复杂度 O(logN) : 二分法为对数级别复杂度。
空间复杂度 O(1) : 几个变量使用常数大小的额外空间。
class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.size() == 0) {return 0;}
int left = 0, right = nums.size() - 1;
int leftIndex, rightIndex;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid - 1;
} else if(nums[mid] <= target){
left = mid + 1;
}
}
rightIndex = left;
if(right >= 0 && nums[right] != target) {return 0;}
left = 0, right = rightIndex;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] >= target){
right = mid - 1;
} else if(nums[mid] < target){
left = mid + 1;
}
}
leftIndex = right;
return rightIndex - leftIndex - 1;
}
};
剑指 Offer 53 - II. 0~n-1中缺失的数字
注意题目的特殊数据状况,可以优化时间复杂度到O(logN)。
解题思路:
排序数组中的搜索问题,首先想到 二分法 解决。
根据题意,数组可以按照以下规则划分为两部分。
左子数组: nums[i]=i ;
右子数组: nums[i]≠i ;
缺失的数字等于 “右子数组的首位元素” 对应的索引;因此考虑使用二分法查找 “右子数组的首位元素” 。
算法解析:
- 初始化: 左边界 i=0 ,右边界 j=len(nums)−1 ;代表闭区间 [i,j] 。
- 循环二分: 当 i≤j 时循环 (即当闭区间 [i,j] 为空时跳出) ;
a. 计算中点 m=(i+j)//2 ,其中 “//” 为向下取整除法;
b. 若 nums[m]=m ,则 “右子数组的首位元素” 一定在闭区间 [m+1,j] 中,因此执行 i=m+1;
c. 若 nums[m]≠m ,则 “左子数组的末位元素” 一定在闭区间 [i,m−1] 中,因此执行 j=m−1 ; - 返回值: 跳出时,变量 i 和 j 分别指向 “右子数组的首位元素” 和 “左子数组的末位元素” 。因此返回 i 即可。
复杂度分析:
时间复杂度 O(logN): 二分法为对数级别复杂度。
空间复杂度 O(1): 几个变量使用常数大小的额外空间。
class Solution {
public:
int missingNumber(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == mid){
left = mid + 1;
} else{
right = mid - 1;
}
}
return left;
}
};