剑指offer 2023

文章介绍了如何在C++中替换字符串中的空格,反向打印链表,以及使用递归和哈希表解决多数元素问题。此外,还探讨了在二维数组中查找特定数值的高效方法,包括枚举、二分查找和矩阵特性利用。最后,讲解了斐波那契数列和青蛙跳台阶问题的解决方案,包括记忆化搜索和动态规划的应用。
摘要由CSDN通过智能技术生成

05.替换空格

需求

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

#示例 1:

输入s = "We are happy."

输出"We%20are%20happy."

代码实现

class Solution {//剑指offer 05.替换空格 
public:
    string replaceSpace(string s)
    {
        //1.遍历字符串s,有空格则count++,count从0开始,用于记录空格数
        int count= 0;
        for( char c : s)//循环字符串s中的每一个字符 
         {
             if(c== ' ')
             {
                 count++;
            }
         }
        //2.用%20代替空格,每个空格(一字符)都被替换成3字符,所以改变字符串长度为 原来长度+空格数count*(3-1)
        int size= s.size();
        s.resize(size+ 2*count);
        //3.使用变量i=旧字符串长度-1,j=新字符串长度-1,从后向前遍历,i没遇到空格就执行s[j]= s[i],遇到空格就把s[j-2]~s[j]替换为%20,再让j-2。循环到i=j时,说明字符串中无空格,跳出循环
        for( int i= size-1,j= s.size()-1; i<j; i--,j--) 
        {
            if(s[i]== ' ')
            {
                s[j-2]= '%';
                s[j-1]= '2';
                s[j]= '0';
                j-= 2;
            } 
            else
            {
                s[j]= s[i];
            }
        }
        return s;
    }
};

思路演示

  1. count变量记录原串中的空格数量,因为空格占一个字符,%20占三个字符。所以计算空格数,以确定替换空格后的新串的长度。

  1. 改变字符串长度为 原来长度+空格数count*(3-1)。

  1. 使用变量i=旧字符串长度-1,j=新字符串长度-1,从后向前遍历。i没遇到空格就执行s[j]= s[i],遇到空格就把s[j-2]~s[j]替换为%20,再让j-2。循环到i=j时,说明字符串中无空格(每替换一次空格,j就更接近i),跳出循环,如图所示。

图0.5-1 替换空格思路演示图


06.从尾到头打印链表

需求

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)

#示例 1:

输入:head = [1,3,2]

输出:[2,3,1]

代码实现

//前置条件-定义链表 
struct ListNode
{
    int val;//链表指针指向的值 
    ListNode *next;
}
//本题说明了递归相当于一个栈顶到栈底的过程 
class solution
{
public:
    //C++ STL中的vector好比是C语言中的数组,但是vector又具有数组没有的一些高级功能。与数组相比,vector就是一个可以不用再初始化就必须制定大小的边长数组,当然了,它还有许多高级功能。
    //如果vector的元素类型是int,默认初始化为0
    vector<int> v1;
    //reversePrint函数返回值是vector定义的int数组 
    vector<int> reversePrint(ListNode* head)
    {
        //如果头节点不为空,那么需要判断头节点的下一节点是否为空,通过递归一直找到某节点的下一节点为空,则该结点为最后一个结点
         if(head!= NULL)
         {
             if(head->next!= NULL)
             {
                 reversePrint(head->next);
            }
            //递归到最后一个结点,此时它就是最后以此递归调用reverse函数的head,将他加入到数组v1中(v1.pushback),最后一次递归执行完成,回到倒数第二次递归,执行(v1.pushback)...所有递归执行完后,返回v1的值,这就是一个从尾到头打印链表的过程 
            v1.push_back(head->val);//此语句即递归的跳出语句,执行后不在有下一次递归,但本方法中前面执行过一半的递归中的 此语句还未执行,全部语句执行完才会结束此方法 
         }
         return v1;
    }
};
/*push_back加入元素,并且这个元素是被加在数组尾部的。
for (int i = 0; i < 20; i++)
{
    v1.push_back(i);
}*/

思路演示

如果头节点不为空,那么需要判断头节点的下一节点是否为空,通过递归一直找到某节点的下一节点为空,则该结点为最后一个结点

执行示意图如下列图所示

图0.6-1 最外层递归

图0.6-2 次外层递归

图0.6-3 最内层递归

通过上面的递归嵌套过程,发现:

在执行最外层递归的push_back语句前,必须执行完次外层push_back语句

在执行次外层递归的push_back语句前,必须执行完最内层push_back语句

即,三次push_back语句的执行过程为:

push_back(8) ------>push_back(2)------>push_back(4)

而原链表的为4-->2-->8

此即反向打印链表的过程


39.数组中出现次数超过一半的数字

int len = nums.size();
        if(len == 1)
        {
            return nums[0];
        }
        else
        {
            for(int i=0;i<len;i++) {
                int count = 1;
                for(int j=i+1;j<len;j++) {
                    if(nums[j] ==nums[i])
                    {
                        count++;
                        if(count > len/2)
                        {
                            return nums[i];
                        }
                    }
                }
            }
            return 0;
        }

上面的方法为最简单的暴力枚举法,但算法复杂度O(n^2)会超出题目所设的时间限制,故要采用时间效率的方法

需求

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

*你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]

输出: 2

代码实现

//方法2 hashmap
class Solution {
public:
    int majorityElement(vector<int>& nums) {
        unordered_map<int,int> counts; //用一个hashmap存储每个元素以及出现的次数 ,建好的是一个空的hashmap ,counts[k]为某个键k的值 
        int majority = 0, cnt = 0; //定义majority变量存储 最多出现的数字,这个数字在更新hashmap的过程中可能会改变 ,定义cnt存储这个最多出现的数字所出现的次数 
        for (int num: nums) { //定义int类型的num遍历nums中的每个元素 
            counts[num]++; //counts[]的[]中即某个键 ,++的意思:如遍历到nums数组中的第一个元素nums[1]=4,则此键4的值加1,即4出现的次数+1 
            if (counts[num] > cnt) { //每次某个键的值+1后,出现次数最多的数字都可能发生更改,因此每次键值+1后都要判断该键的值有没有超过之前值最大的键的值
                majority = num;  //判断出键值+1的键超过之前的最大键值, 将该数字赋给majority 
                cnt = counts[num]; //判断出键值+1的键超过之前的最大键值, 将该数字出现的次数赋给cnt 
            }
        }
        return majority;
    }
};

思路演示

因为题设中给定 “你可以假设数组是非空的,并且给定的数组总是存在多数元素。”所以可以建立一个哈希表储存每个数字即其对应出现的次数,再通过遍历哈希表找出值最大的键并返回,而此题解用的是动态更新目标元素及其所出现的次数,即在每一次更新哈希表后分别存储目标元素及其所出现的次数到两个自己定义的变量中,最后直接返回存储目标元素的变量即可。

#unordered_map<int,int>是C++ STL标准库中的一个容器,它是一个哈希表,用于存储键值对。其中,键和值都是整数类型。它的特点是可以快速地进行查找、插入和删除操作,时间复杂度为O(1)。与map不同的是,unordered_map中的元素是无序的。

#unordered_map<,>的<,>中的元素也可以是其他类型,如unordered_map<char,bool>

#unordered_map<,>,每个键都对应一个相应的值

具体过程如下:

  1. 用一个hashmap存储每个元素以及出现的次数 ,建好的是一个空的hashmap ,counts[k]为某个键k的值 。

  1. 定义majority变量存储 最多出现的数字,这个数字在更新hashmap的过程中可能会改变 ,定义cnt存储这个最多出现的数字所出现的次数。

  1. 定义int类型的num遍历nums中的每个元素。

  1. counts[ ]的[ ]中填入的即某个键 ,++的意思:如遍历到nums数组中的第一个元素nums[1]=4,则此键4的值加1,即4出现的次数+1 。

  1. 每次某个键的值+1后,出现次数最多的数字都可能发生更改,因此每次键值+1后都要判断该键的值有没有超过之前值最大的键的值。

  1. 判断出键值+1的键超过之前的最大键值, 将该数字赋给majority 。

  1. 判断出键值+1的键超过之前的最大键值, 将该数字出现的次数赋给cnt 。

  1. 遍历完成后,return majority,即返回出现次数最多的数字。

#注:因为建表的空间复杂度为O(n),所以建表的时间复杂度不超过O(n),而查表的时间复杂度为O(1),所以本算法的时间复杂度为O(n)


03.数组中重复的数字

需求

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

#示例 1:

输入:[2, 3, 1, 0, 2, 5, 3]

输出:2 或 3

代码实现

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        unordered_map<int,int> counts;
        for(int num:nums) {
            counts[num]++;
            if(counts[num] == 2){
                return num;
            }
        }
        return -1;
    }
};

思路演示

建立一个哈希表,动态记录每个数字出现的次数,当检测到第一个重复出现的次数,返回该数即可


04.二维数组中的查找

需求

在一个 n * m 的二维数组中,每一行都按照从左到右 非递减 的顺序排序,每一列都按照从上到下 非递减 的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

[

[1, 4, 7, 11, 15],

[2, 5, 8, 12, 19],

[3, 6, 9, 16, 22],

[10, 13, 14, 17, 24],

[18, 21, 23, 26, 30]

]

给定 target = 5,返回 true。

给定 target = 20,返回 false。

代码实现

方法1.枚举

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        //思路:执行一个嵌套循环,外层循环第某行,内层循环第某行的第某个元素 找到返回True,全部遍历后找不到返回False
        for(int i=0; i<matrix.size(); i++){
            for(int j=0; j<matrix[0].size(); j++) {
                if(target == matrix[i][j]){
                    return true;
                }
            }
        }
        return false;
    }
};

*方法2.对每行进行二分查找

#include<algorithm>

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        for(const auto row : matrix) {
            auto it = lower_bound(row.begin(), row.end(), target);
            if(it != row.end() && *it == target) {
                return true;
            }
        }
        return false;
    }
};

方法3.用本题矩阵的单调不递减性

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        //初始化为左下角
        int row = matrix.size();
        //int column = matrix[0].size;这句加上会报错,因为vector为空时没有matrix[0]
        int i = row-1;
        int j = 0;
        
        while(i>=0 && j<matrix[0].size()) {
            //小于target,target在右方,删除列
            if(matrix[i][j] < target) {
                j++;
            }
            //大于target,target在上方,删除行
            else if(matrix[i][j] > target) {
                i--;
            }
            else {
                return true;
            }
        }
        return false;
    }
};

思路演示

方法1是枚举。执行一个嵌套循环,外层循环第某行,内层循环第某行的第某个元素 找到返回True,全部遍历后找不到返回False。

若使用暴力法遍历矩阵 matrix ,则时间复杂度为

O(NM) 。暴力法未利用矩阵 “从上到下递增、从左到右递增” 的特点,显然不是最优解法。


方法2对每行进行二分查找 。

由于用matrix[0].size( )方法时解决不了矩阵为空的报错问题,因此借用auto完成对迭代器的访问

复杂度分析

时间复杂度:O(nlogm)。

推算过程:

假设序列里共有n个元素,

第一次,在n个元素内找到目标元素;

第二次,n/2个元素内找到目标元素;

第三次,n/(22)个元素内找到目标元素;

······

第k次,n/(2k)个元素内找到目标元素。

因为2k<=n,所以当且仅当n/(2k)>=1时,k最大为log2n。

时间复杂度O(h)=O(log2n).

对一行使用二分查找的时间复杂度为 O(logm),最多需要进行 n 次二分查找。

空间复杂度:O(1)。

一:

auto:在块作用域、命名作用域、循环初始化语句等等 中声明变量时,关键词auto用作类型指定符。

const:修饰符

​ 想要拷贝元素:for(auto x:range)

​ 想要修改元素 : for(auto &&x:range)

​ 想要只读元素:for(const auto& x:range)

auto

​ auto即 for(auto x:range) 这样会拷贝一份range元素,而不会改变range中元素;

但是!(重点) 使用for(auto x:vector)时得到一个proxy class,操作时会改变vector本身元素。应用:for(bool x:vector)

auto&:

​ 当需要修改range中元素,用for(auto& x:range)

当vector返回临时对象,使用auto&会编译错误,临时对象不能绑在non-const l-value reference (左值引用)需使用auto&&,初始化右值时也可捕获

const auto&:

​ 当只想读取range中元素时,使用const auto&,如:for(const auto&x:range),它不会进行拷贝,也不会修改range

const auto:

​ 当需要拷贝元素,但不可修改拷贝出来的值时,使用 for(const auto x:range),避免拷贝开销

附加案例:迭代器iterator访问

看下面的例子,对std::map容器的iterator访问,书写起来非常复杂,如果嵌套了多层STL容器就更加复杂了。

std::map<std::string, int> myMap;
for (std::map<std::string, int>::iterator it = myMap.begin(); it != myMap.end(); it++)
{
    std::cout << it->first << endl;
}

这时候,使用auto来替代迭代器一堆的代码,就显得很简洁:

std::map<std::string, int> myMap;
for (auto it = myMap.begin(); it != myMap.end(); it++)
{
    std::cout << it->first << endl;
}

二:

lower_bound()函数需要加载头文件#include<algorithm>,其基本用途是查找有序区间中第一个大于或等于某给定值的元素的位置,其中排序规则可以通过二元关系来表示。

函数原型:

template<class ForwardIterator, class Type>

ForwardIterator lower_bound(

ForwardIterator _First,

ForwardIterator _Last,

const Type& _Val

);

template<class ForwardIterator, class Type, class BinaryPredicate>

ForwardIterator lower_bound(

ForwardIterator _First,

ForwardIterator _Last,

const Type& _Val,

BinaryPredicate _Comp

);

传入参数说明

_First 要查找区间的起始位置

_Last 要查找区间的结束位置

_Val 给定用来查找的值

_Comp 自定义的表示小于关系的函数对象,根据某个元素是否满足小于关系而返回true或者false


方法3用本题矩阵的单调不递减性。

如下图所示,我们将矩阵逆时针旋转 45° ,并将其转化为图形式,发现其类似于 二叉搜索树 ,即对于每个元素,其左分支元素更小、右分支元素更大。因此,通过从 “根节点” 开始搜索,遇到比 target 大的元素就向左,反之向右,即可找到目标值 target 。

从矩阵 matrix 左下角元素(索引设为 (i, j) )开始遍历,并与目标值对比:

当 matrix[i][j] > target 时,执行 i-- ,即消去第 i 行元素;

当 matrix[i][j] < target 时,执行 j++ ,即消去第 j 列元素;

当 matrix[i][j] = target 时,返回 true ,代表找到目标值。

若行索引或列索引越界,则代表矩阵中无目标值,返回 false 。

每轮 i 或 j 移动后,相当于生成了“消去一行(列)的新矩阵”, 索引(i,j) 指向新矩阵的左下角元素(标志数),因此可重复使用以上性质消去行(列)。

复杂度分析:

时间复杂度 O(M+N) :其中,

N 和 M 分别为矩阵行数和列数,此算法最多循环 M+N 次。

空间复杂度 O(1) : i, j 指针使用常数大小额外空间。


10-I.斐波那契数列

需求

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0, F(1) = 1

F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

代码实现

1.递归,实现简单,但会超时加上无法实现结果对1e9+7取模

class Solution {
public:
    int fib(int n)
    {
        if(n==1)
        {
            return 1;
        }
        else if(n==0)
        {
            return 0;
        }
        else
        {
            return fib(n-1)+fib(n-2);
        }
    }
};

2.递归会超时,记忆化搜索+递归不会超时如计算f(9)=f(8)+f(7)时若f(8)已经在其他运算里算过,则会直接返回,而不是再计算一次f(8)

class Solution {
    const int mod=1000000007;
    int dp[101]={0};//dp数组存已经计算过的数
public:
    int fib(int n)
    {
        if(dp[n])
        {
            return dp[n];//存过dp[n],if判断为true,直接返回dp[n]
        }
        if(n<2)
        {
            return n;
        }
        dp[n]=(fib(n-1)+fib(n-2))%mod;
        return dp[n];
    }
};

2.用轮转三个数a,b,c的方式不断得出fibonacci数列的下一个数

class Solution {
public:
    int fib(int n)
    {
        if(n<2)
        {
            return n;//下标小于2,直接返回
        }
        
        int a,b,c;
        a=0; b=1; c=1;//初始化
        
        for(int i=2; i<n; i++)
        {
            a=b;
            b=c;
            c=(a+b)%1000000007;//要在这而不能在 return c,因为c是int,在a和b大到一定程度时,int类型不能装下a+b
        }
        return c;
    }
};

10-II.青蛙跳台阶问题

需求

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2

输出:2

示例 2:

输入:n = 7

输出:21

示例 3:

输入:n = 0

输出:1

代码实现

  1. 记忆化搜索+递归

class Solution {
    const int mod = 1000000007
    int dp[101] = 0//dp数组存已经计算过的数
public:
    int numWays(int n)
    {
        if(dp[n])
        {
            return dp[n];
        }//当前台阶数的跳法已被求过,直接返回该结果 
        
        if(n<2)
        {
            return 1;
        }//当前台阶数为一或零,可以有的跳法均返回1(为零代表跳两格到第二格,为一代表从跳一格到第二格)
        
        dp[n]=(numWays(n-1)+numWays(n-2))%mod;
        return dp[n];
    }
};

2.动态规划

class Solution {
    const int mod = 1000000007;
    int dp[101] = {0};
public:
    int numWays(int n)
    {
        dp[0] = 1;//0级,即没有台阶 
        dp[1] = 1;
        dp[2] = 2;//上三行初始化dp数组
        
        for(int i=3; i <= n; i++)
        {
            dp[i] = (dp[i-1] + dp[i-2])%mod;    
        } //求出了n级台阶一共的跳法数的递推方程
        return dp[n]; 
    }
}; 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值