坚持刷题,争取每周天更新
剑指 Offer 03. 数组中重复的数字
解法1:使用unordered_map
要找到任意一个,没有限定第一个,或者第k个,直接遍历一边数组,每次遍历时将元素插入到map中,直到map中出现重复值代表找到
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
unordered_map<int , bool> map;//构造时bool默认为false
for(int num:nums){//遍历
if(map[num]) //如果在map中已经存在该元素
{
return num;
}
map[num] = true;//添加进去的元素设置为true
}
return -1;
}
};
时空复杂度都为O(n)
解法二:
题目中的数字在一个长度为n的数组中,且数字也在0到n-1这个范围内,可以肯定的是一定有重复,且考虑将数组中每个元素的位置与下标对应,那么一定有两个元素的下标对应到了一起(因为有重复的)
如下2应该被映射到num[2],而num[4]存储的2页会被映射到num[2],此时就发生了冲突即num[i]==num[num[i]],即找到了该元素
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;
}
};
//作者:jyd
此时时间O(n),空间O(1)
剑指 Offer 04. 二维数组中的查找
行递增,列递增,最小元素在左上角,最大元素在右下角,可以这样解决,从右上角出发向左,当该位置值大于target时向左,当该位置小于target时向下,为什么这么想呢?因为将该矩阵旋转45°就变成了一颗二叉搜索树了。
当然从左上角也可以走,就是稍微麻烦点
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
if(!matrix.empty()){//判断vector是否为空
int row = matrix.size()-1;//行数
int col = matrix[0].size();//列数
int i = 0;
int j = col-1;
while(i <= row && j >= 0){
if(target==matrix[i][j]){
return true;
}
else if(matrix[i][j] > target){//向左
--j;
}
else{ //向下
++i;
}
}
}
return false;
}
};
时间复杂度O(m+n)m为行数,n为列数,空间复杂度O(1)
剑指 Offer 05. 替换空格
解法一:
再开辟一个string来保存结果,将原字符串值依次判断再添加到新串中
class Solution {
public:
string replaceSpace(string s) {
string res;
for(char c:s){
if(c==' '){
res+="%20";
}else{
res+=c;
}
}
return res;
}
};
时间上遍历一次O(n),空间上开辟了一个新的串O(n)
解法二:
不用开辟一个新的string,而是在已有的string后面再加上需要扩充的空间
需要扩充的空间为:空格数*2
然后再利用双指针,从后向前扫,普通字符直接修改,空格则连续修改3个位置
class Solution {
public:
string replaceSpace(string s) {
int length = s.length();
int count = 0;//替换之后的长度
for(int i=0;i<length;i++){
if(s[i]==' ') count++;
}
int new_length = length + count*2;
s.resize(new_length);//重新扩容
int low = length;
int high = new_length; //指向最高位
while(low >= 0 && high > low)
{
if(s[low] == ' '){//如果是空格的话
s[high--] = '0';
s[high--] = '2';
s[high--] = '%';
}else{
s[high--] = s[low];
}
--low;
}
return s;
}
};
该方法应为没有开辟新的串所以空间上为O(1)
剑指 Offer 06. 从尾到头打印链表
方法一:
使用vector的insert方法,可以直接实现链表的头插法,但是缺点是运行的时间复杂度有点高,因为每次都要实现数组元素的移动
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int>res;
while(head!=NULL){
res.insert(res.begin(),head->val);//头插法
head = head->next;
}
return res;
}
};
时间O(n^2)空间O(n)
方法二:
使用了C++算法库中的reverse方法,直接实现逆置
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> res;
while(head) {
res.push_back(head->val);
head = head->next;
}
reverse(res.rbegin(), res.rend());
return res;
}
};
复杂度:reverse()函数无返回值,时间复杂度O(n)
方法三:
只要设计到逆序,FILO这种问题自然想到用桟或者递归,本题将元素依次插入桟中,再出栈即实现了逆置
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
stack<int>s;//定义一个桟
while(head){
s.push(head->val);//元素入栈
head = head->next;
}
vector<int>res(s.size());//避免动态扩容的开销
for(auto it=res.begin();it!=res.end();it++){
*it = s.top();
s.pop();
}
return res;
}
};
出入桟都是O(1),总时间复杂度为O(n),空间上开辟了桟和vector也是O(n)
剑指 Offer 09. 用两个栈实现队列
桟是先进后出,队列是先进先出,利用两个桟分别完成一次入栈即可实现队列,如ABCD进SA,再DCBA进SB,此时SB的出栈顺序已经是一个队列的顺序了,需要注意的是,只有出栈时我们才需要将SA移动至SB
class CQueue {
public:
stack<int> stack1;
stack<int> stack2;
CQueue() {}
void appendTail(int value) {
stack1.push(value);
}
int deleteHead() {
if (stack1.empty()) return -1;
while (!stack1.empty()){ // SA -> SB 模拟队列
int tmp = stack1.top();
stack1.pop();
stack2.push(tmp);
}
// 删除栈顶元素(即队首出队)
int res = stack2.top();
stack2.pop();
while (!stack2.empty()){ // SA <- SB 放回SA
int temp = stack2.top();
stack2.pop();
stack1.push(temp);
}
return res;
}
};
当碰到连续删除时,上面的代码每次都需要再把SB拷贝回SA,这时我们可以在插入时再考回SA
class CQueue {
public:
stack<int> stack1;
stack<int> stack2;
CQueue() {}
void appendTail(int value) {
while (!stack2.empty()){ // 1 <- 2
int temp = stack2.top();
stack2.pop();
stack1.push(temp);
}
stack1.push(value);
}
int deleteHead() {
if (stack1.empty()&&stack2.empty()) return -1;
while (!stack1.empty()){ // 1 -> 2
int tmp = stack1.top();
stack1.pop();
stack2.push(tmp);
}
// delete head
int res = stack2.top();
stack2.pop();
return res;
}
};
时间复杂度:对于插入和删除操作,时间复杂度均为 O(1)O(1)。插入不多说,对于删除操作,虽然看起来是 O(n)的时间复杂度,但是仔细考虑下每个元素只会「至多被插入和弹出 stack2 一次」,因此均摊下来每个元素被删除的时间复杂度仍为 O(1)O(1)。
空间复杂度:O(n)O(n)。需要使用两个栈存储已有的元素。
剑指 Offer 10- I. 斐波那契数列
老生常谈的一道题了属于是,后一次的结果由前两次结果得出
解法一:我的解法
class Solution {
public:
int fib(int n) {
//递归
if(n==0) return 0;
if(n==1) return 1;
int f1=0,f2=1,res;
for(int i=2;i<=n;i++){
res = (f1 + f2)% 1000000007;//后一次结果
f1 = f2; //分别更新前两次结果
f2 = res;
}
return res ;
}
};
!!!解法二:记忆化递归
一点理解:如果不使用记忆数组,那么我们上一次计算得到的值计算机其实并不会保留,再往下算时又需要再计算一次,这样就增加了许多重复的工作
当我们引入记忆数组后,上一次的值保存在cache[n-1],cache[n-2]中,再计算n时,直接根据n-1和n-2中的值可以直接得出结论
//递归解决子问题。但要注意的是递归时会产生大量重复计算,可能会超时,所以使用一个记忆数组避免重复计算。
class Solution {
public:
int cache[101];
int fib(int n) {
if(n < 2){
return n;
}
if(cache[n] != 0) return cache[n];
cache[n] = (fib(n-1) + fib(n-2)) % (int)(1e9 + 7);
return cache[n];
}
};
时空复杂度都是O(n)
剑指 Offer 10- II. 青蛙跳台阶问题
和上面的fib基本一模一样
class Solution {
public:
int numWays(int n) {
if(n==0||n==1) return 1;
int res,step1=1,step2=1;
for(int i=2;i<=n;i++){
res = (step1+step2)%1000000007;
step1 = step2;
step2 = res;
}
return res;
}
};
时间上O(n),空间上O(1)
剑指 Offer 11. 旋转数组的最小数字
本题主要想考察的就是二分的方法
解法一:直接for循环O(n)
class Solution {
public:
int minArray(vector<int>& numbers) {
int min = -2147483647;
min = numbers.front();
for(auto it=numbers.begin();it!=numbers.end();++it){
if((*it)<min) min = (*it);
}
return min;
}
};
解法二:二分法
如果mid大于high代表mid的右边是从左边旋转过去的,最小的在右边故 low = mid +1,否则在左边
class Solution {
public:
int minArray(vector<int>& numbers) {
int low=0 , high = numbers.size()-1;
while(low<high){
int mid = (low+high)/2;
if(numbers[mid]>numbers[high]) low = mid + 1;
else if (numbers[mid]<numbers[high]) high = mid;
else high--;
}
return numbers[low];
}
};
剑指 Offer 15. 二进制中1的个数
解法一:逐位想与再统计次数
理解:用C++的位运算,每次将该数右移一位(保证无符号),然后和1相与,结果为1代表该位为1,再统计个数
class Solution {
public:
int hammingWeight(uint32_t n) {
int res=0;
for(int i=0;i<=31;i++){
if( ((n>>i)&1)==1) res++;
}
return res;
}
};
解法二:
用n不断&n-1,直到为0即可统计出0的个数
为何可以这样算呢?原因很简单,每减一之后最右边的1就会被抵消,比如1在最低位,减1后变0,相与后抵消了最低位的1,假如1在次低位:10减1后为01 01与10相与后为00这样次低位的1页被抵消掉了。
这让我想起了补码的快速计算:从右向左找到第一个1,左侧取反右侧不变,因为这个1一定是低位进上来的
count=0
while(k){
k=k&(k-1);
count++;
}
流程大概是这样的
231. 2 的幂(和上面类似的一题)
解法一:
问题还是要全面的考虑,一开始的思路错的太离谱了,边界条件都没搞明白,好不容易整明白了,又没仔细看数据范围,测试数据中有负数的,所以一定要转成unsigned int才好使
class Solution {
public:
bool isPowerOfTwo(int n) {
int count=0;
n = (unsigned)n;
if(n>0){
while(n){
n = n&(n-1);//算出1的个数
count++;
}
}
if(count==1) return true;
return false;
}
};
解法二:继续优化
解法一存在一个小问题,就是进行了太多次的判断,其实没必要,只需要进行一次判断就可以得出结论,只要n&(n-1)不全为0,则代表一定不是2的整数次幂
class Solution {
public:
bool isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
};
为什么一次就能判断呢?因为如果你是二的次幂,那么你必定只有1个1,即与n-1相与一定为0