剑指offer刷题时参考答案解析进行的总结,感谢力扣题解区的大神们,如果有侵权,随时联系我,我会立马删除,感谢
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
限制:2 <= n <= 100000
由于数字范围都在0~n-1之内,考虑利用一个大小和nums相同的布尔数组记录某个数字是否出现过,如果当前数字没有出现过(对应flag为false),将对应flag标记为true;如果当前数字对应flag为true,说明出现过,将其输出。目前为止100%
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
bool flag[nums.size()];
memset(flag, false, sizeof(flag));
for(int i = 0; i < nums.size(); i++)
if(flag[nums[i]])
return nums[i];
else
flag[nums[i]] = true;
return -1; //返回值是必须的,以防某种情况下没有返回值。
}
};
在一个 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。
清华电子本科转行刷题学基础,30天leetcode从0到100题,春招面试做出hard,拿到一线厂核心部门和二线厂算法offer,我的自学笔记和题解,我的知乎,欢迎找我交流、内推(邮箱/知乎咨询/私信)。
关键在于起点的选取,从左下角或者右上角开始
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int i=matrix.size()-1,j=0;
while(i>=0&&j<matrix[0].size())
if(matrix[i][j]==target) return true;
else if(matrix[i][j]>target) i--;
else j++;
return false;
}
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:
输入:s = “We are happy.”
输出:“We%20are%20happy.”
限制:0 <= s 的长度 <= 10000
解题思路
没有开辟额外空间,先根据空格数量在字符串末尾扩容两个字符的空间(因为一个空格变为%20需要多出两个空间),
然后倒叙遍历将原来位置的字符放到后面, 最后返回s就可以了.
代码
class Solution {
public:
string replaceSpace(string s) {
int l1 = s.length() - 1;
for (int i = 0; i <= l1; i++) {
if (s[i] == ' ') {
s += "00";
}
}
int l2 = s.length() - 1;
if (l2 <= l1) {
return s;
}
for (int i = l1; i >= 0; i--) {
char c = s[i];
if (c == ' ') {
s[l2--] = '0';
s[l2--] = '2';
s[l2--] = '%';
} else {
s[l2--] = c;
}
}
return s;
}
};
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
限制:
0 <= 链表长度 <= 10000
解题思路
4种解法:reverse反转法、堆栈法、递归法、改变链表结构法
在编写代码前,要跟面试官沟通清楚,根据面试官提出的不同性能需求(时间效率优先 or 空间效率优先 or 不允许改变链表结构 or 云云),给出不同的算法。
下面的代码思路清晰,注释都很好理解。
代码
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/
class Solution {
public:
vector<int> res;
vector<int> reversePrint(ListNode* head) {
//方法1:reverse反转法
/*
while(head){
res.push_back(head->val);
head = head->next;
}
//使用algorithm算法中的reverse反转res
reverse(res.begin(),d());
return res;
*/
//方法2:入栈法
/*
stack<int> s;
//入栈
while(head){
s.push(head->val);
head = head->next;
}
//出栈
while(!s.empty()){
res.push_p());
s.pop();
}
return res;
*/
//方法3:递归
/*
if(head == nullptr)
return res;
reversePrint(head->next);
res.push_back(head->val);
return res;
*/
//方法4:改变链表结构
ListNode *pre = nullptr;
ListNode *next = head;
ListNode *cur = head;
while(cur){
next = cur->next;//保存当前结点的下一个节点
cur->next = pre;//当前结点指向前一个节点,反向改变指针
pre = cur;//更新前一个节点
cur = next;//更新当前结点
}
while(pre){//上一个while循环结束后,pre指向新的链表头
res.push_back(pre->val);
pre = pre->next;
}
return res;
}
};
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/
9 20
/
15 7
限制:
0 <= 节点个数 <= 5000
代码
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/class Solution {public: TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { //递归分治 return recursionBuild(preorder.begin(),d(),inorder.begin(),d());
//wzhibao:这里要注意d()指向的是容器的最后一个元素的后一个位置。
} //递归分治 TreeNode* recursionBuild(vector<int>::iterator preBegin, vector<int>::iterator preEnd,vector<int>::iterator inBegin, vector<int>::iterator inEnd ) { if(inEnd==inBegin) return NULL; TreeNode* cur = new TreeNode(*preBegin); auto root = find(inBegin,inEnd,*preBegin); cur->left = recursionBuild(preBegin+1,preBegin+1+(root-inBegin),inBegin,root); cur->right = recursionBuild(preBegin+1+(root-inBegin),preEnd,root+1,inEnd); return cur; }};
鉴于有些同学跟我讨论的
cur->left = recursionBuild(preBegin+1,preBegin+1+(root-inBegin),inBegin,root);
中第二个参数加不加一的问题,更新了如下解释
其前提是,输入的参数区间是前闭后开区间
当然,不加1确实能运行成功,但这是程序的一个bug:
用上面的例子来说,如果重建左子树时,不加1,那么输入的区间是[1,1)与[0,1):
那么首先,输入函数的前序区间与中序区间长度是不等的,这逻辑上就不合理
至于为什么能运行成功,是因为以下原因:
if(inEnd==inBegin) return NULL;程序只会判断输入的中序区间前后是否相等而不会检查前序区间
因此,上述输入前序区间[1,1)时,虽然区间什么元素都没有但仍然不会停止运行,将继续执行下一TreeNode* cur = new TreeNode(*preBegin);
这个时候,由于我们传入的是原向量的引用,所以*preBegin仍然能正确取到值,即preoder[1]
所以程序仍然能够正确运行
所以呢,重建左子树时,第二个参数还是要加一的!
感兴趣的同学不妨在if(inEndinBegin) return NULL;后加一句if(preBeginpreEnd) return NULL;试试,就明白了。
此时,如果不加1,那么将会得到错误答案。
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
示例 1:
输入:
[“CQueue”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:
输入:
[“CQueue”,“deleteHead”,“appendTail”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]
class CQueue {stack<int>s1;stack<int>s2;
public:CQueue() {```
}
void appendTail(int value) {s1.push(value);
}
int deleteHead() {pty()){pty()){return -1;}else{while(!s1.empty()){s2.p());s1.pop();} }}int headp();s2.pop();return head;
}
```
};
/**
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 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:
输入:n = 2
输出:1
示例 2:
输入:n = 5
输出:5
class Solution {
public:long long fib(unsigned int n) {int result[2]={0,1};if(n<2)return result[n];unsigned long long Num1=1;unsigned long long Num2=0;unsigned long long Final=0;for(int i=2;i<=n;i++){Final=(Num1+Num2)%1000000007;//这里要求是取余,对于每个结果都要去取余,过程中就是,而不只是在最终结果。Num2=Num1;Num1=Final;}// while(Final>1e9+7)// Final=Final%1000000007;return Final;}
};
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
题解:
class Solution {
public:
int numWays(int n) {
int result[2]={1,2};
if(n==0)
return 1;//这里要注意 在0阶时 默认是等于1
if(n<=2)
return result[n-1];
long long Num1=1;
long long Num2=2;
long long Final=0;
for(int i=3;i<=n;i++)
{
Final=Num1+Num2;
Final=Final%1000000007;
Num1=Num2;
Num2=Final;
}
return Final;
}
};
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
示例 1:
输入:[3,4,5,1,2]
输出:1
示例 2:
输入:[2,2,2,0,1]
输出:0
class Solution
{
public:
int minArray(vector<int>& numbers)
{
int length=numbers.size();
if(numbers[0]<numbers[length-1])
return numbers[0];
int begin=0;
int end=length-1;
int mid=(begin+end)/2;
while(numbers[0]>=numbers[length-1])
{
if(end-begin==1)
return numbers[end];
if(numbers[begin]==numbers[mid]&&numbers[end]==numbers[mid])
{
return minCom(numbers,begin,end);
}
if(numbers[mid]>=numbers[begin])
{
begin=mid;
mid=(begin+end)/2;
}
else if(numbers[mid]<=numbers[end])
{
end=mid;
mid=(begin+end)/2;
}
}
return 1;
}
int minCom(vector<int>& numbers,int begin,int end)
{
int result=numbers[begin];
for(int i=1;i<=end;i++)
{
if(result>numbers[i])
result=numbers[i];
}
return result;
}
};
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[[“a”,“b”,“c”,“e”],
[“s”,“f”,“c”,“s”],
[“a”,“d”,“e”,“e”]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“a”,“b”],[“c”,“d”]], word = “abcd”
输出:false
int x[] = {-1,0,0,1};
int y[] = {0,-1,1,0};
class Solution {
public:int row,col;bool dfs(vector<vector<char>>& board,string word,int i,int j,int idx) {if (idx == word.size()) return true;char tmp = board[i][j];board[i][j] = '.';for (int k = 0; k < 4; k++) {int d_x = x[k] + i;int d_y = y[k] + j;if (d_x >= 0 && d_x < row && d_y >= 0 && d_y < col && word[idx] == board[d_x][d_y]) {if (dfs(board,word,d_x,d_y,idx + 1)) return true;}}board[i][j] = tmp;return false;}bool exist(vector<vector<char>>& board, string word) {row = board.size();col = board[0].size();for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {if (board[i][j] == word[0]) {if (dfs(board,word,i,j,1)) return true;}}}return false;}
};
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[['A','B','C','E'],['S','F','C','S'],['A','D','E','E']
]给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false
class Solution {
public:bool exist(vector<vector<char>>& board, string word) {for(int i = 0; i < board.size(); i++){for(int j = 0; j < board[0].size(); j++){if(backtrack(board, word, 0, i , j)){ // 从二维表格的每一个格子出发return true;}}}return false;}
private:bool backtrack(vector<vector<char>>& board, string& word, int wordIndex, int x, int y){if( board[x][y] != word[wordIndex]){ // 当前位的字母不相等,此路不通return false;}if(word.size() - 1 == wordIndex){ // 最后一个字母也相等, 返回成功return true;}char tmp = board[x][y]; board[x][y] = 0; // 避免该位重复使用wordIndex++;if((x > 0 && backtrack(board, word, wordIndex, x - 1, y)) // 往上走 (此处多谢笑川兄指正)|| (y > 0 && backtrack(board, word, wordIndex, x, y - 1)) // 往左走|| (x < board.size() - 1 && backtrack(board, word, wordIndex, x + 1, y)) // 往下走|| (y < board[0].size() - 1 && backtrack(board, word, wordIndex, x, y + 1))){ // 往右走return true; // 其中一条能走通,就算成功}board[x][y] = tmp; // 如果都不通,则回溯上一状态return false;}
};
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
思路和算法
我们将行坐标和列坐标数位之和大于 k 的格子看作障碍物,那么这道题就是一道很传统的搜索题目,我们可以使用广度优先搜索或者深度优先搜索来解决它,本文选择使用广度优先搜索的方法来讲解。
那么如何计算一个数的数位之和呢?我们只需要对数 x 每次对 10 取余,就能知道数 x 的个位数是多少,然后再将 x 除 10,这个操作等价于将 x 的十进制数向右移一位,删除个位数(类似于二进制中的 >> 右移运算符),不断重复直到 x 为 0 时结束。
同时这道题还有一个隐藏的优化:我们在搜索的过程中搜索方向可以缩减为向右和向下,而不必再向上和向左进行搜索。如下图,我们展示了 16 * 16 的地图随着限制条件 k 的放大,可行方格的变化趋势,每个格子里的值为行坐标和列坐标的数位之和,蓝色方格代表非障碍方格,即其值小于等于当前的限制条件 k。我们可以发现随着限制条件 k 的增大,(0, 0) 所在的蓝色方格区域内新加入的非障碍方格都可以由上方或左方的格子移动一步得到。而其他不连通的蓝色方格区域会随着 k 的增大而连通,且连通的时候也是由上方或左方的格子移动一步得到,因此我们可以将我们的搜索方向缩减为向右或向下。
class Solution {// 计算 x 的数位之和(重要)int get(int x) {int res=0;for (; x; x /= 10) {res += x % 10;}return res;}
public:int movingCount(int m, int n, int k) {if (!k) return 1;queue<pair<int,int> > Q; (将两个数据组合成一个数据)// 向右和向下的方向数组int dx[2] = {0, 1};int dy[2] = {1, 0};vector<vector<int> > vis(m, vector<int>(n, 0));//定义一个二维数组,vector<int>为数据类型,m表示有m行,vector<int>(n,0),表示这个容器中有n个0,属于初始化操作。Q.push(make_pair(0, 0));vis[0][0] = 1;int ans = 1;while (!Q.empty()) {auto [x, y] = Q.front();Q.pop();for (int i = 0; i < 2; ++i) {int tx = dx[i] + x;int ty = dy[i] + y;if (tx < 0 || tx >= m || ty < 0 || ty >= n || vis[tx][ty] || get(tx) + get(ty) > k) continue;Q.push(make_pair(tx, ty));vis[tx][ty] = 1;ans++;}}return ans;}
};
复杂度分析
时间复杂度:O(mn)O(mn),其中 m 为方格的行数,n 为方格的列数。考虑所有格子都能进入,那么搜索的时候一个格子最多会被访问的次数为常数,所以时间复杂度为 O(2mn)=O(mn)O(2mn)=O(mn)。
空间复杂度:O(mn)O(mn),其中 m 为方格的行数,n 为方格的列数。搜索的时候需要一个大小为 O(mn)O(mn) 的标记结构用来标记每个格子是否被走过。
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
class Solution {
public:int cuttingRope(int n) {// n<=3的情况,m>1必须要分段,例如:3必须分成1、2;1、1、1 ,n=3最大分段乘积是2, 同理2的最大乘积为1if (n == 2)return 1;if (n == 3)return 2;vector<int> dp(n+1);//vector的定义和数组的定义是一致的,n+1即代表有n+1个数/*下面3行是n>=4的情况,跟n<=3不同,4可以分很多段,比如分成1、3,这里的3可以不需要再分了,因为3分段最大才2,不分就是3。记录最大的。*/dp[1] = 1;dp[2] = 2;dp[3] = 3;for(int i = 4; i <= n; i++) {int maxValue = 0;//记录最大的//j<=i/2是因为1*3和3*1是一样的,没必要计算在内,只要计算到1*3和2*2就好了for(int j = 1; j <= i/2; j++) {maxValue = max(maxValue, dp[j] * dp[i-j]);}dp[i] = maxValue;}return dp[n];}
};
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m] 。请问 k[0]k[1]…*k[m] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
题解:
class Solution {public:
int cuttingRope(int n) {
if(n==2) return 1;
if(n==3) return 2;
int Count1=n/3;
if(n-Count1*3==1)
Count1=Count1-1;
int Count2=(n-Count1*3)/2;
long long end1=1;
for(int i=1;i<=Count1;i++)
{
end1=end1*3%1000000007;
}
int end=end1*(int)pow(2,Count2)%1000000007;
return end;
}
};
注解:
大数阶乘,大数的排列组合等,一般都要求将输出结果对1000000007取模(取余)
为什么总是1000000007呢= =
1.1000000007是一个质数(素数),对质数取余能最大程度避免冲突~
2.int32位的最大值为2147483647,所以对于int32位来说1000000007足够大
3.int64位的最大值为2^63-1,对于1000000007来说它的平方不会在int64中溢出
所以在大数相乘的时候,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出
其实不止1e9+7,还有1e9+9和998244353。这三个数都是一个质数,同时小于 。所以有什么好处呢?
1.所有模过之后的数在加法操作在int范围内不会溢出,即 。
2.在乘法操作后在long long范围内不会溢出,即 。
二进制中1的个数
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
示例 1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。
示例 2:
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 ‘1’。
示例 3:
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 ‘1’。
class Solution {
public:
int hammingWeight(uint32_t n) {
if(n==0) return 0;
int count=0;
while(n)
{
n=n&(n-1);
count++;
}
return count;
}
};
实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。
示例 1:
输入: 2.00000, 10
输出: 1024.00000
示例 2:
输入: 2.10000, 3
输出: 9.26100
示例 3:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25
class Solution {
public:double myPow(double x, int n) {if(x == 1 || n == 0) return 1;double ans = 1;long num = n;if(n < 0){num = -num;x = 1/x;}while(num){if(num & 1) ans *= x;x *= x;num >>= 1;}return ans;}
};
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
示例 1:
输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
说明:
用返回一个整数列表来代替打印
n 为正整数
class Solution {
public:
//这个问题的考点是大数问题
//打印数字的主函数
vector<int> res;
vector<int> printNumbers(int n)
{
//输入检查
if(n <= 0) return res;
//分配内存
char* number = new char[n + 1]; //这里注意使用字符串表示数字
memset(number, '0', n);
number[n] = '