文章目录
-
- 说明
- 剑指 Offer 03. 数组中重复的数字
- 剑指 Offer 04. 二维数组中的查找
- 剑指 Offer 05. 替换空格
- 剑指 Offer 06. 从尾到头打印链表
- 剑指 Offer 07. 重建二叉树
- 剑指 Offer 09. 用两个栈实现队列
- 剑指 Offer 10- I. 斐波那契数列【1】
- 剑指 Offer 10- II. 青蛙跳台阶问题【1】
- 剑指 Offer 11. 旋转数组的最小数字
- 剑指 Offer 12. 矩阵中的路径
- 剑指 Offer 14- I. 剪绳子
- 剑指 Offer 14- II. 剪绳子 II
- 剑指 Offer 15. 二进制中1的个数
- 剑指 Offer 16. 数值的整数次方
- 剑指 Offer 17. 打印从1到最大的n位数
- 剑指 Offer 18. 删除链表的节点
- 剑指 Offer 19. 正则表达式匹配
- 剑指 Offer 20. 表示数值的字符串【1】
- 剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
- 剑指 Offer 22. 链表中倒数第k个节点
- 剑指 Offer 24. 反转链表
- 剑指 Offer 25. 合并两个排序的链表
- 剑指 Offer 26. 树的子结构
- 剑指 Offer 27. 二叉树的镜像
- 剑指 Offer 28. 对称的二叉树
- 剑指 Offer 29. 顺时针打印矩阵
- 剑指 Offer 30. 包含min函数的栈
- 剑指 Offer 31. 栈的压入、弹出序列
- 剑指 Offer 32 - I. 从上到下打印二叉树
- 剑指 Offer 32 - II. 从上到下打印二叉树 II
- 剑指 Offer 32 - III. 从上到下打印二叉树 III
- 剑指 Offer 33. 二叉搜索树的后序遍历序列
- 剑指 Offer 34. 二叉树中和为某一值的路径
- 剑指 Offer 35. 复杂链表的复制
- 剑指 Offer 36. 二叉搜索树与双向链表
- 剑指 Offer 37. 序列化二叉树【0】
- 剑指 Offer 38. 字符串的排列
- 剑指 Offer 39. 数组中出现次数超过一半的数字
- 剑指 Offer 40. 最小的k个数
- 剑指 Offer 41. 数据流中的中位数【0】
- 剑指 Offer 42. 连续子数组的最大和
- 剑指 Offer 43. 1~n 整数中 1 出现的次数【0】
- 剑指 Offer 44. 数字序列中某一位的数字【0】
- 剑指 Offer 46. 把数字翻译成字符串
- 剑指 Offer 47. 礼物的最大价值
- 剑指 Offer 48. 最长不含重复字符的子字符串
- 剑指 Offer 49. 丑数
- 剑指 Offer 50. 第一个只出现一次的字符
- 剑指 Offer 51. 数组中的逆序对【1】
- 剑指 Offer 52. 两个链表的第一个公共节点
- 剑指 Offer 53 - I. 在排序数组中查找数字 I
- 剑指 Offer 53 - II. 0~n-1中缺失的数字
- 剑指 Offer 54. 二叉搜索树的第k大节点
- 剑指 Offer 55 - I. 二叉树的深度
- 剑指 Offer 55 - II. 平衡二叉树
- 剑指 Offer 56 - I. 数组中数字出现的次数
- 剑指 Offer 56 - II. 数组中数字出现的次数 II【1】
- 剑指 Offer 57. 和为s的两个数字
- 剑指 Offer 57 - II. 和为s的连续正数序列
- 剑指 Offer 58 - I. 翻转单词顺序
- 剑指 Offer 58 - II. 左旋转字符串
- 剑指 Offer 59 - I. 滑动窗口的最大值
- 剑指 Offer 60. n个骰子的点数
- 剑指 Offer 62. 圆圈中最后剩下的数字
- 剑指 Offer 63. 股票的最大利润
- 剑指 Offer 64. 求1+2+…+n
- 剑指 Offer 65. 不用加减乘除做加法
- 剑指 Offer 66. 构建乘积数组
- 剑指 Offer 64. 求1+2+…+n
- 面试题13. 机器人的运动范围
- 面试题45. 把数组排成最小的数
- 面试题59 - II. 队列的最大值
- 面试题61. 扑克牌中的顺子
说明
0-还未做,先略过
1-待尝试其它方法
剑指 Offer 03. 数组中重复的数字
【数组,元素&索引的对应关系】
题干:
给定一个长度为n的数组nums,里面的所有数字都在 [ 0 , n − 1 ] [0,n-1] [0,n−1]内,找出数组中任意一个重复数字并返回。
数据范围:2<=n<=1e5
函数签名:
int findRepeatNumber(vector<int>& nums);
解:
- 方法1:哈希
- 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
- 方法2:元素与索引一一对应
- 对于这种数组元素在 [ 0 , n − 1 ] [0,n-1] [0,n−1]范围的问题,可以将值为i的元素调整到第i个位置上进行求解;在调整过程中,如果第i个位置上已经有一个值为i的元素,就可以知道i值重复了。
- 原地修改,时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
for循环版:
int findRepeatNumber(vector<int>& nums) {
for(int i = 0; i< nums.size(); i++){
while(nums[i]!= i){
if(nums[nums[i]] == nums[i]){
return nums[i];
}
swap(nums[i], nums[nums[i]]);
}
}
return -1;
}
while循环版:
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 04. 二维数组中的查找
【二分&抽象BST】
题干:给定一个m*n的二维数组,每行按从左到右非递减,每列从上到下非递减的顺序排序。给定一个整数target,判断数组中是否含有该整数。
数据范围:0<=m,n<=1e3
函数签名:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target)
解:
- 方法1:二分
- 遍历每行,对每列二分,判断能否找到target
- 时间复杂度 O ( m l o g n ) O(mlogn) O(mlogn)(若遍历列二分行则 O ( n l o g m ) O(nlogm) O(nlogm)),空间复杂度 O ( 1 ) O(1) O(1)
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
if(!matrix.size() || !matrix[0].size()) return false;
int m = matrix.size(), n = matrix[0].size();
for(int i = 0; i < m; i++){
int left = 0, right = n;
int index = 0;
while(left < right){
int mid = left + ((right - left) >> 1);
if(matrix[i][mid]== target) return true;
else if(matrix[i][mid] > target) right = mid;
else left = mid+1;
}
}
return false;
}
- 方法2:抽象BST
- 该题的matrix从左到右递增,从上到下递增,所以左上角是最小元素,右下角是最大元素。若想高效在matrix中搜索一个元素,肯定要从某个角开始(如左上角),每次只能向右或向下移动,不要走回头路。
- 但左上角无论向右还是向下,元素值都会增加;右下角开始同理
- 所以从右上角或左下角开始就可以了。如规定从右上角开始,每次只能向左或向下移动,这样就能根据当前位置的元素和target的大小关系判断如何移动,不断接近从而找到target的位置
- 其实这就是一棵以右上角为根节点的BST
- 若target<当前节点,搜索当前节点的左子树,即col–;
- 若target>当前节点,搜索当前节点的右子树,即row++。
- 时间复杂度 O ( m + n ) O(m+n) O(m+n),空间复杂度 O ( 1 ) O(1) O(1)
- 该题的matrix从左到右递增,从上到下递增,所以左上角是最小元素,右下角是最大元素。若想高效在matrix中搜索一个元素,肯定要从某个角开始(如左上角),每次只能向右或向下移动,不要走回头路。
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
if(!matrix.size() || !matrix[0].size()) return false;
int m = matrix.size(), n = matrix[0].size();
int i = 0, j = n-1;
while(i < m && j >= 0){
if(target == matrix[i][j]) return true;
else if(target < matrix[i][j]) j--;
else i++;
}
return false;
}
剑指 Offer 05. 替换空格
【双指针】
题干:把字符串s的每个空格替换成"%20"
数据范围:0<=s.length()<=1e5
函数签名:
string replaceSpace(string s);
解:
- 方法1:另开空间
- 另开一个string变量,遍历s,遇到空格时往里添加"%20",遇到非空格时添加s[i]。
- 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
string replaceSpace(string s) {
string res;
int j = 0;
for(char c: s){
if(c == ' ') res+="%20";
else res += c;
}
return res;
}
- 方法2:双指针,原地修改
- 每替换一个空格,字符串长度增加2,则可以先统计s中空格的总数,就可计算出替换后的字符串的总长度(
s.length()+2
) - 从字符串的末尾开始复制和替换,实现原地修改。
- 双指针i,j,一个指向原字符串末尾,一个指向替换后的字符串末尾,根据s[i]是否等于空格进行复制或替换操作
- 若空格都替换完了,则i==j,可退出循环
- 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
- 每替换一个空格,字符串长度增加2,则可以先统计s中空格的总数,就可计算出替换后的字符串的总长度(
string replaceSpace(string s) {
int cnt = 0;
for(char c: s){
if(c == ' ') ++cnt;
}
int len = s.size();
s.resize(len + 2*cnt);
int i = len-1, j = s.size()-1;
while(i != j){
if(s[i] != ' '){
s[j] = s[i];
i--, j--;
}
else{
s[j--] = '0';
s[j--] = '2';
s[j--] = '%';
i--;
}
}
return s;
}
剑指 Offer 06. 从尾到头打印链表
【递归,二叉树后序遍历】
题干:输入一个链表的头节点,从尾到头反过来返回每个节点的值。
数据范围:0<=链表长度<=1e5
函数签名:
vector<int> reversePrint(ListNode* head)
解:
- 方法:递归
- 类似二叉树的后序遍历。
- 对于每个节点,先访问(打印)它的子节点的值,再打印该节点的值,所以是在后序位置写打印语句。
- 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)
vector<int> reversePrint(ListNode* head) {
vector<int> res;
printLink(head, res);
return res;
}
void printLink(ListNode* head, vector<int>& res){
if(!head) return;
printLink(head->next, res);
res.push_back(head->val);
}
剑指 Offer 07. 重建二叉树
【递归】
题干:通过二叉树的前序序列和中序序列重建二叉树
数据范围:0<=节点个数<=5e3
函数签名:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder)
解:
- 方法:递归,前序位置建根节点。见之前的文章。
- 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)
- 时间:初始化hashMap占用 O ( n ) O(n) O(n);递归建立n个节点,每层递归中的节点建立、搜索操作占用 O ( n ) O(n) O(n)。因此使用 O ( n ) O(n) O(n)时间。
- 空间:hashMap使用 O ( n ) O(n) O(n)额外空间;最差情况下(二叉树为链表时),递归深度 O ( n ) O(n) O(n),占用 O ( n ) O(n) O(n)的栈帧空间。因此总共使用 O ( n ) O(n) O(n)空间
- 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)
- 注意条件:不含重复元素。
unordered_map<int, int> mp;
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
for(int i = 0; i < inorder.size(); i++) mp[inorder[i]] = i;
int n = preorder.size();
return build(preorder, 0, n-1, inorder, 0, n-1);
}
TreeNode* build(vector<int>& preorder, int preL, int preR, vector<int>& inorder, int inL, int inR){
if(preL > preR) return nullptr;
int rootVal = preorder[preL];
TreeNode* root = new TreeNode(rootVal);
int inRootIndex = mp[rootVal];
int leftSize = inRootIndex - inL;
root->left = build(preorder, preL+1, preL+1 + leftSize-1, inorder, inL, inRootIndex-1);
root->right = build(preorder, preL+ leftSize + 1, preR, inorder, inRootIndex+1, inR);
return root;
}
剑指 Offer 09. 用两个栈实现队列
【模拟】
题干:用两个栈实现队列的push和pop操作(pop不成功返回-1)
数据范围:1<=val<=1e4
,最多进行1e4
次操作
函数签名:
class CQueue {
public:
CQueue() {
}
void appendTail(int value) {
}
int deleteHead() {
}
};
解:
- 方法:
- “in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序”(来源)。
class CQueue {
private:
stack<int> st1, st2;
public:
CQueue() {
}
void appendTail(int value) {
st1.push(value);
}
int deleteHead() {
if(!st2.empty()){
int val = st2.top();
st2.pop();
return val;
}
while(!st1.empty()){
int val = st1.top();
st1.pop();
st2.push(val);
}
if(st2.empty()) return -1;
int val = st2.top();
st2.pop();
return val;
}
};
剑指 Offer 10- I. 斐波那契数列【1】
【递归,递推,矩阵快速幂(待整理)】
题干:求斐波那契数列(f[0]=0,f[1]=1, f[n]=f[n-1]+f[n-2]
),并对答案取模(1e9+7)
数据范围:0<=n<=100
函数签名:
int fib(int n);
解:
- 方法1:递归+记忆化搜索
const int mod = 1e9+7;
static const int N = 100;
int f[N+2];
int fib(int n){
if(n == 0) return 0;
if(n == 1) return 1;
if(f[n]) return f[n];
f[n] = (fib(n-1) + fib(n-2)) % mod;
return f[n];
}
- 方法2:递推
const int mod = 1e9+7;
static const int N = 100;
int f[N+2];
int fib(int n) {
int f[n+2];
f[0] = 0;
f[1] = 1;
for(int i = 2; i <= n; i++){
f[i] = (f[i-1] + f[i-2]) % mod;
}
return f[n];
}
剑指 Offer 10- II. 青蛙跳台阶问题【1】
【递归,递推,矩阵快速幂(待整理)】
题干:青蛙往上跳台阶,一次可以跳1级也可以跳2级,求青蛙跳上一个n级的台阶共有多少种跳法,答案对1e9+7取模。
数据范围:0<=n<=100
函数签名:
int numWays(int n);
解:
- 方法1:递归+记忆化搜索
const int mod = 1e9+7;
static const int N = 100;
int dp[N+5];
int numWays(int n) {
if(n == 0) return 1;
if(n == 1) return 1;
if(dp[n]) return dp[n];
dp[n] = (numWays(n-1) + numWays(n-2)) % mod;
return dp[n];
}
- 方法2:递推
const int mod = 1e9+7;
static const int N = 100;
int dp[N+5];
int numWays(int n){
dp[0]=1;
dp[1]=1;
for(int i = 2; i<= n; i++){
dp[i] = (dp[i-1] + dp[i-2]) % mod;
}
return dp[n];
}
- 优化:空间压缩
const int mod = 1e9+7;
int numWays(int n){
int dp_0 = 1, dp_1 = 1;
for(int i = 2; i<= n; i++){
int dp_2 = (dp_0 + dp_1) % mod;
dp_0 = dp_1;
dp_1 = dp_2;
}
return dp_1;
}
剑指 Offer 11. 旋转数组的最小数字
【二分】
题干:给定一个不降序数组numbers
(里面可以有相同元素),现将numbers
从 k k k位置( k k k未知)旋转(即将从 k k k开始到末尾的部分移到最前面),要求在numbers
中寻找到最小的元素并返回元素值。
数据范围:1<=numbers.length<=5e3, -5e3<=numbers[i]<=5e3
函数签名:
int minArray(vector<int>& numbers);
解:
- 方法:二分,见之前的文章。
int minArray(vector<int>& numbers) {
int left = 0, right = numbers.size()-1;
if(left == right) return numbers[0]; // boundary
while(numbers[left] == numbers[right] && (left != right)) left++;
int target = numbers[right];
right++;
int ans=0;
while(left < right){
int mid = left + ((right - left)>>1);
if(numbers[mid]<=target){
ans = numbers[mid];
right = mid;
}
else left = mid + 1;
}
return ans;
}
剑指 Offer 12. 矩阵中的路径
【DFS回溯】
题干:给定一个二维字符网格board和一个字符串word,判断word是否在board中存在,存在返回true,不存在返回false。
数据范围:1<=m,n<=6, 1<=word.length()<=15
函数签名:
bool exist(vector<vector<char>>& board, string word);
解:
- 方法:DFS递归+回溯
- 使用vis数组标记,防止重复搜索
bool exist(vector<vector<char>>& board, string word) {
m = board.size(), n = board[0].size();
for(int i = 0; i < m;i ++){
for(int j = 0; j < n; j++){
if(board[i][j] == word[0]){
if(dfs(board, word, i, j, 0)) return true;
}
}
}
return false;
}
private:
bool vis[10][10];
int m, n;
int dx[4] = {
-1, 1, 0, 0};
int dy[4] = {
0, 0, -1, 1};
bool dfs(vector<vector<char>>& board, string word, int x, int y, int pos){
if(pos == word.size()) return true;
if(x<0 || x >= m || y<0 || y >= n){
return false;
}
if(vis[x][y] || word[pos] != board[x][y]) return false;
vis[x][y] = 1;
for(int i = 0; i<= 3; i++){
int newX = x + dx[i];
int newY = y + dy[i];
if(dfs(board, word, newX, newY, pos+1)){
return true;
}
}
vis[x][y] = 0;
return false;
}
剑指 Offer 14- I. 剪绳子
【DP,贪心+快速幂】
题干:给定一根长度为n的绳子,把它剪成整数长度的m段(m必须大于1),问这m的乘积最大可能是多少?
数据范围:2<=n<=58
函数签名:
int cuttingRope(int n);
解:
- 方法:dp
- 设长度为n的绳子cut的最大乘积为dp(n),
- 方法1:递归+记忆化搜索
int cuttingRope(int n){
return dp(n);
}
private:
int memo[60];
int dp(int n){
if(n==0 || n==1) return 0;
if(memo[n]) return memo[n];
for(int i = 1; i < n; i++){
memo[n] = max(memo[n], max(i*(n-i), i*dp(n-i)));
}
return memo[n];
}
- 方法2:递推
int cuttingRope(int n){
vector<int> dp(n+2);
for(int i = 2; i <= n; i++){
for(int j = 1; j < i; j++){
dp[i] = max(dp[i], max(j*(i-j), j*dp[i-j]));
}
}
return dp[n];
}
剑指 Offer 14- II. 剪绳子 II
【贪心+快速幂】
解:
- 方法:贪心
- 要取模,DP要比较最大值,所以不行
- 一个整数n切分(至少切分成两段),使得乘积最大:尽可能多的3,不行就换2
- 数学证明略
const int p = 1e9+7;
long long qmod(long long a, long long b){
long long res = 1;
while(b){
if(b&1) res = res * a %p;
a = a * a %p;
b>>=1;
}
return res;
}
int cuttingRope(int n) {
if(n == 2) return 1;
if(n == 3) return 2;
int cnt = n/3;
int mod = n%3;
int ans = 1;
if(!mod){
ans = qmod(3ll, cnt) %p;
}
else if(mod == 1){
ans = qmod(3ll, cnt-1) * 4ll % p;
}
else{
ans = qmod(3ll, cnt) * 2ll % p;
}
return ans;
}
剑指 Offer 15. 二进制中1的个数
【位运算】
解:
- 方法1:快速幂的思路
int hammingWeight(uint32_t n) {
int cnt = 0;
while(n){
if(n&1) ++cnt;
n>>=1;
}
return cnt;
}
- 方法2:n&(n-1):把最后一个1变为0
int hammingWeight(uint32_t n) {
int cnt = 0;
while(n){
++cnt;
n = n & (n-1);
}
return cnt;
}
剑指 Offer 16. 数值的整数次方
【快速幂,浮点数】
解:
- 方法:快速幂
- 注意对浮点数的处理(指数为负数时)
double myPow(double x, int n){
if(!x) return 0;
long long b = n;
if(n < 0){
x = 1.0/x;
b = -b;
}
double res = 1.0;
while(b){
if(b & 1) res = res * x;
x = x * x;
b >>=1;
}
return res;
}
剑指 Offer 17. 打印从1到最大的n位数
【dfs,全排列,元素无重可复选】
解:
-
方法:递归,DFS全排列
-
大数,利用string转int。
-
int转string:to_string
-
string s = to_string(x);
-
-
string转int:atoi+c_str
-
大数,利用string。转int需要去除前导0,所以用atoi+s.c_str()转为int
-
区别:
- 使用atoi,不全为字符串转为0
- 使用stoi,不全为字符串会报错
-
int x = atoi(s.c_str()); //先将string转为const char*,再转为int
-
-
-
bool flag = 0;
vector<int> ans;
vector<int> printNumbers(int n) {
string s = "0123456789";
string path = "";
dfs(s, path, n);
return ans;
}
void dfs(string &s, string &path, int res){
if(res == 0){
if(!flag){
//去掉0
flag = 1;
return;
}
ans.push_back(atoi(path.c_str()));
return;
}
for(int i = 0; i< s.length(); i++){
path += s[i]; //path.push_back(s[i]);
dfs(s, path, res-1);
path.pop_back();
}
}
剑指 Offer 18. 删除链表的节点
【链表删除】
解:
判断的是cur->next是否为要删除的节点,注意判空
ListNode* deleteNode(ListNode* head, int val) {
ListNode* dummy = new ListNode(-1);
dummy->next = head;
ListNode* cur = dummy;
while(cur && cur->next){
//while(cur->next)
if(cur->next->val == val){
cur->next = cur->next->next;
break;
}
cu