剑指offer 刷题记录
No1 二维数组中的查找
题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2, 8, 9],
[2,4, 9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。
给定 target = 3,返回 false。
解题思路
二维数组左上角元素是最小的,右下角元素是最大的,从左下角向上逐渐减小,向右逐渐增大,可以采用二分法,从左下角开始比较。
- 如果当前元素比target小,则row--;
- 如果当前元素比target大,则额col++;
- 如果相等,返回true;
- 如果越界还没找到没,说明不存在,返回false。
class Solution {
public:
bool Find(int target, vector<vector<int> > array) {
if(array.size() == 0)
// 如果行数为0
return false;
if(array[0].size() == 0)
// 如果列数为0
return false;
int row = array.size();
int col = array[0].size();
int i = row-1;
int j = 0;
while(i >= 0 && j < col){
if(array[i][j] > target)
i--;
else if(array[i][j] < target)
j++;
else
return true;
}
return false;
}
};
No2 替换空格
题目描述
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
解题思路
int length;
length指的是字符串实际长度(不包含'\0')
题目有歧义,无法判断内存空间是否足够存放替换后的字符串,若不能存放,需要另开辟空间。
答案假设可以存放。
/*
问题1:替换字符串,是在原来的字符串上做替换,还是新开辟一个字符串做替换!
问题2:在当前字符串替换,怎么替换才更有效率。
从前往后替换,后面的字符要不断往后移动,要多次移动,所以效率低下
从后往前,先计算需要多少空间,然后从后往前移动,则每个字符只移动一次,这样效率更高一点。
*/
class Solution {
public:
void replaceSpace(char *str,int length) {
int spaceConut = 0;
for(int i = 0; i < length; i++){
if(str[i] == ' '){
spaceConut++;
}
}
// totalLen需要的总长度
int totalLen = length + spaceConut * 2;
str[totalLen] = '\0';
for(int i = length - 1; i >=0; i--){
if(str[i] == ' '){
str[--totalLen] = '0';
str[--totalLen] = '2';
str[--totalLen] = '%';
}
else
str[--totalLen] = str[i];
}
}
};
N0.3 从尾到头打印链表
题目描述
输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。

解题思路
这题很简单。。从前向后保存,然后reverse就可以了
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
if(head == nullptr)
return vector<int>();
vector<int> ret;
while(head != nullptr){
ret.emplace_back( head->val);
head = head->next;
}
reverse(ret.begin(), ret.end());
return ret;
// return vector<int>(ret.rbegin(), ret.rend()); // 或者直接返回反转容器
}
};
No4 重建二叉树
题目描述
给定节点数为 n 的二叉树的前序遍历和中序遍历结果,请重建出该二叉树并返回它的头结点。
例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出如下图所示。

解题思路
- step 1:先根据前序遍历第一个点就是根节点,建立根节点。
- step 2:然后遍历中序遍历找到根节点所在位置。
- step 3:再按照子树的节点数将两个遍历的序列分割成子数组,将子数组送入函数建立子树。
- step 4:直到子树的序列长度为0,结束递归。
/**
* Definition for binary tree
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
// 若先序遍历、中序遍历都为0(或者有一个为0),即空二叉树
int n = pre.size();
int m = vin.size();
if(n == 0 && m ==0){
return nullptr;
}
TreeNode *root = new TreeNode(pre[0]);
for(int i = 0; i < m; i++){
// 在中序遍历中找到先序遍历的第一个元素
if(pre[0] == vin[i]){
// 左子树的先序遍历
vector<int> leftpre(pre.begin() + 1, pre.begin() + i + 1);
// 左子树的中序遍历
vector<int> leftvin(vin.begin(), vin.begin() + i);
// 构建左子树
root->left = reConstructBinaryTree(leftpre, leftvin);
// 右子树的先序遍历
vector<int> rightpre(pre.begin()+ i + 1, pre.end());
// 右子树的中序遍历
vector<int> rightvin(vin.begin()+ i + 1, vin.end());
// 构建右子树
root->right = reConstructBinaryTree(rightpre, rightvin);
break;
}
}
return root;
}
};
No5 用两个栈来实现一个队列
题目描述
用两个栈来实现一个队列,使用n个元素来完成 n 次在队列尾部插入整数(push)和n次在队列头部删除整数(pop)的功能。 队列中的元素为int类型。保证操作合法,即保证pop操作时队列内已有元素。
解题思路

class Solution
{
public:
// 入队列正常入栈
void push(int node) {
stack1.push(node);
}
int pop() {
// 将所有栈1 元素 反向放入 栈2
// 将栈1的最上方元素放入栈2 栈1出栈
while(!stack1.empty()){
stack2.push(stack1.top());
stack1.pop();
}
// pop指令 弹出top元素
int res = stack2.top();
stack2.pop();
// 再将栈2元素 反向放回 栈1
while(!stack2.empty()){
stack1.push(stack2.top());
stack2.pop();
}
return res;
}
private:
stack<int> stack1;
stack<int> stack2;
};
No6 旋转数组
题目描述
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
数据范围:1 ≤ n ≤ 10000 1 ≤ n ≤10000,数组中任意元素的值: 0 ≤ val ≤ 10000 0 ≤ val ≤ 10000
要求:空间复杂度:O(1),时间复杂度:O(logn)
解题思路
解法1:这不就是找最小值么,常规解法,但是时间复杂度为O(n);或者使用sort排序容器
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {
if(rotateArray.size() == 0)
return 0;
int res = rotateArray[0];
for(int i = 1; i < rotateArray.size(); i++){
if(res > rotateArray[i]){
res = rotateArray[i];
}
}
return res;
}
};
解法2:二分法

class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {
int left = 0;
int right = rotateArray.size() - 1;
while(left < right){
int mid = (left + right) / 2;
//最小的数字在mid右边
if(rotateArray[mid] > rotateArray[right])
left = mid + 1;
//无法判断,一个一个试
else if(rotateArray[mid] == rotateArray[right])
right--;
//最小数字要么是mid要么在mid左边
else
right = mid;
}
return rotateArray[left];
}
};
No7 斐波那契数列
题目描述

解题思路
class Solution {
public:
int Fibonacci(int n) {
if(n == 1 || n == 2)
return 1;
if(n == 3)
return 2;
// 可以直接使用三个元素的容器进行轮换存放即可
vector<int> nums(3);
nums[0] = 1;
nums[1] = 1;
nums[2] = 2;
// 需要第几项 就直接算到第几项
for(int i = 3; i < n; i++){
nums[i % 3] = nums[(i - 1) % 3] + nums[(i - 2) % 3];
}
return nums[(n - 1) % 3];
}
};
No8 跳台阶
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
解题思路
找规律,和斐波那契数列解法一致
f(1) = 1;
f(2) = 2;
f(3) = 3;
f(4) = 5;
f(n) = f(n - 1) + f(n - 2);
class Solution {
public:
int jumpFloor(int number) {
if(number <= 0) return 0;
if(number == 1) return 1;
if(number == 2) return 2;
vector<int> nums(3);
nums[0] = 1;
nums[1] = 2;
for(int i = 2; i <= number; i++){
nums[i % 3] = nums[(i - 1) % 3] + nums[(i - 2) % 3];
}
return nums[(number - 1) % 3];
}
};
No9 跳台阶扩展版
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解题思路
讲解豁然开朗:因为n级台阶,第一步有n种跳法:跳1级、跳2级、到跳n级 跳1级,剩下n-1级,则剩下跳法是f(n-1) 跳2级,剩下n-2级,则剩下跳法是f(n-2) 所以f(n)=f(n-1)+f(n-2)+...+f(1) 因为f(n-1)=f(n-2)+f(n-3)+...+f(1) 所以f(n)=2*f(n-1)
递归算法:
class Solution {
public:
int jumpFloorII(int number) {
if(number == 1)
return 1;
// f(n) = 2*f(n-1)
return 2 * jumpFloorII(number -1);
}
};
No10 矩阵覆盖
题目描述
我们可以用 2*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2*1 的小矩形无重叠地覆盖一个 2*n 的大矩形,从同一个方向看总共有多少种不同的方法?

解题思路
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
对于斐波那契数列的递推公式:f(n) = f(n−1) + f(n−2);
除了自顶向下递归求解,还可以直接自底向上相加,递归的本质是从顶部往下找,然后再向上相加,我们可以使用动态规划直接相加。
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= number; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
但是这个方法使用了dp数组,空间复杂度为O(n),我们还可以对空间优化一下:注意到每次循环只使用到了第i−1
个变量和第i−2
个变量,那我们可以用两个变量不断滚动来优化。
class Solution {
public:
int rectCover(int number) {
if(number == 0) return 0;
if(number == 1) return 1;
if(number == 2) return 2;
int dp1 = 1;
int dp2 = 2;
int res = 0;
for(int i = 3; i <= number; i++){
res = dp1 + dp2;
// 更新变量,将2放入1,将res放入2
dp1 = dp2;
dp2 = res;
}
return res;
}
};
No11 二进制中1的个数
题目描述
输入一个整数 n ,输出该数32位二进制表示中1的个数。其中负数用补码表示。
解题思路
将移位后的1与数字进行位与运算,结果为1就记录一次。
class Solution {
public:
int NumberOf1(int n) {
int res = 0;
//遍历32位
for (int i = 0; i < 32; i++) {
//将移位后的1与原数值按位比较
if ((n & (1 << i)) != 0)
res++;
}
return res;
}
};
牛客大神做法:如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。
举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。
这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。
class Solution{
public:
int NumberOf1(int n){
int res = 0;
while(n != 0){
n = n & (n - 1);
res++;
}
}
};
No12 数值的整数次方
题目描述
- 求一个浮点数的整数次方
- 整数有正有负
- 不可以使用库函数,也不需要判断大数问题
解题思路
直接循环相乘即可,注意次方小于0 的情况,转换成 x的-2次为(1/x)的2次方。
class Solution {
public:
double Power(double base, int exponent) {
double res = 1;
if (exponent < 0) {
base = 1 / base;
exponent = -exponent;
}
for (int i = 0; i < exponent; i++) {
res *= base;
}
return res;
}
};
快速幂算法
class Solution {
public:
double myPow(double base, int exponent) {
if (exponent == 0)
return 1;
if (base == 0.0)
return 0;
long N = exponent;
if (exponent < 0)
N = -exponent;
double res = 1.0;
while (N > 0) {
if (N % 2 != 0)
// 为奇次幂就需要补充一个x
res *= base;
base *= base; // 将得到的次幂相乘
N >>= 1; // 即N/2
}
return exponent < 0 ? 1 / res : res;
}
};
No13 调整数组顺序使奇数位于偶数前面
题目描述
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
解题思路
暴力解法,新开辟一个数组保存数据
class Solution {
public:
void reOrderArray(vector<int> &array) {
vector<int> arr;
for(const int v : array){
if(v & 1)
arr.push_back(v);
}
for(const int v : array){
if(!(v & 1))
arr.push_back(v);
}
copy(arr.begin(), arr.end(), array.begin());
}
};
No14 链表中倒数第k个结点
题目描述
输入一个链表,输出该链表中倒数第k个结点。
解题思路
输出倒数第k个结点就是输出正数第n-k个结点,先遍历计节点数,再遍历计数第n-k个,注意0结点链表和k>n的情况
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(!pListHead || k <= 0)
return nullptr;
int n = 0;
ListNode * cur = pListHead;
while(cur){
cur = cur->next;
++n;
}
if(n < k)
return nullptr;
n = n - k;
while(n--){
pListHead = pListHead->next;
}
return pListHead;
}
};
解法2:快慢指针
使用快慢指针,首先让快指针先行k步,然后让快慢指针每次同行一步,直到快指针指向空节点,慢指针就是倒数第K个节点。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(!pListHead || k <= 0)
return nullptr;
auto slow = pListHead, fast = pListHead;
while(k--){
if(fast)
fast = fast->next;
else
// 如果k > n 即fast->nullprt;
return nullptr;
}
while(fast){
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
No15 反转链表
题目描述
给定一个单链表的头结点pHead
(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
数据范围:0≤n≤1000

解题思路
解法1:先对原链表做头删操作,再对新链表做头插
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(!pHead)
return nullptr;
ListNode * newHead = nullptr;
ListNode * node = nullptr;
while(pHead){
// 原链表头删
// 保存当前节点
// 头节点后移
node = pHead;
pHead = pHead->next; // 先将head指向下一个,因为等下会改变原head的指向
// 新链表头插
// 指向新的头节点
// 头节点后移
node->next = newHead; // 注意使用node来切换指向
newHead = node;
}
return newHead;
}
};
解法2:使用三个节点不断移位
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(!pHead)
return nullptr;
ListNode * p0 = nullptr;
ListNode * p1 = pHead;
ListNode * p2 = pHead->next;
while(p1){
p1->next = p0;
p0 = p1;
p1 = p2;
if(p2)
p2 = p2->next;
}
return p0;
}
};
原理同上,只是新增加一个局部变量,代替了原来的p2指针
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
ListNode * cur = pHead;
ListNode * pre = NULL;
while(cur){
ListNode * temp;
temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};
No16 合并两个有序链表
题目描述
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
解题思路
初始化:定义cur指向新链表的头结点
操作:
- 如果l1指向的结点值小于等于l2指向的结点值,则将l1指向的结点值链接到cur的next指针,然后l1指向下一个结点值
- 否则,让l2指向下一个结点值
- 循环步骤1,2,直到l1或者l2为nullptr
- 将l1或者l2剩下的部分链接到cur的后面技巧
技巧
一般创建单链表,都会设一个虚拟头结点,也叫哨兵,因为这样每一个结点都有一个前驱结点。
迭代版本求解
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
ListNode * vhead = new ListNode(-1);
ListNode * cur = vhead;
while(pHead1 && pHead2){
if(pHead1->val <= pHead2->val){
cur->next = pHead1;
pHead1 = pHead1->next;
}
else{
cur->next = pHead2;
pHead2 = pHead2->next;
}
cur = cur->next;
}
cur->next = pHead1 ? pHead1 : pHead2;
return vhead->next;
}
};
递归版本求解
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
if(!pHead1) return pHead2;
if(!pHead2) return pHead1;
if(pHead1->val <= pHead2->val){
pHead1->next = Merge(pHead1->next, pHead2);
return pHead1;
}
else{
pHead2->next = Merge(pHead1, pHead2->next);
return pHead2;
}
}
};
No17 树的子结构
题目描述
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
解题思路
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
//如果A是空树 或者 B是空树,直接返回false
if(!pRoot1 || !pRoot2)
return false;
// A的根节点和B的根节点相同,依次比较他的子节点
bool flag1 = isSub(pRoot1, pRoot2);
// 判断B是否是 A左子树的子结构
bool flag2 = HasSubtree(pRoot1->left, pRoot2);
// 判断B是否是 A右子树的子结构
bool flag3 = HasSubtree(pRoot1->right, pRoot2);
// 最终返回值
return flag1 || flag2 || flag3;
}
bool isSub(TreeNode* A, TreeNode* B){
// 迭代遍历完B时,说明B和A匹配
if(B == nullptr)
return true;
// 若此时B不为空,A为空表示不匹配
if(A == nullptr)
return false;
// 若A与B不相等,直接返回false
if(A->val != B->val)
return false;
// 如果A节点与B节点相等,迭代判断A B子树
return isSub(A->left, B->left) && isSub(A->right, B->right);
}
};
代码优化:
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
//如果A是空树 或者 B是空树,直接返回false
if(!pRoot1 || !pRoot2)
return false;
// A的根节点和B的根节点相同,依次比较他的子节点
// 判断B是否是 A左子树的子结构
// 判断B是否是 A右子树的子结构
// 最终返回值
return isSub(pRoot1, pRoot2) || HasSubtree(pRoot1->left, pRoot2) || HasSubtree(pRoot1->right, pRoot2);
}
bool isSub(TreeNode* A, TreeNode* B){
// 迭代遍历完B时,说明B和A匹配
if(B == nullptr)
return true;
// 若此时B不为空,A为空表示不匹配
if(A == nullptr)
return false;
// 若A与B不相等,直接返回false
if(A->val != B->val)
return false;
// 如果A节点与B节点相等,迭代判断A B子树
return isSub(A->left, B->left) && isSub(A->right, B->right);
}
};
No22 从上往下打印二叉树
题目描述
不分行从上往下打印出二叉树的每个节点,同层节点从左至右打印。例如输入{8,6,10,#,#,2,1},如以下图中的示例二叉树,则依次打印8,6,10,2,1(空节点不打印,跳过),请你将打印的结果存放到一个数组里面,返回。
解题思路
知识点:队列
队列是一种仅支持在表尾进行插入操作、在表头进行删除操作的线性表,插入端称为队尾,删除端称为队首,因整体类似排队的队伍而得名。它满足先进先出的性质,元素入队即将新元素加在队列的尾,元素出队即将队首元素取出,它后一个作为新的队首。
思路:
二叉树的层次遍历就是按照从上到下每行,然后每行中从左到右依次遍历,得到的二叉树的元素值。对于层次遍历,我们通常会使用队列来辅助:
因为队列是一种先进先出的数据结构,我们依照它的性质,如果从左到右访问完一行节点,并在访问的时候依次把它们的子节点加入队列,那么它们的子节点也是从左到右的次序,且排在本行节点的后面,因此队列中出现的顺序正好也是从左到右,正好符合层次遍历的特点。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) { }
};*/
class Solution {
public:
vector<int> PrintFromTopToBottom(TreeNode* root) {
vector<int> res;
if(root == nullptr)
return res;
queue<TreeNode*> q;
q.push(root);
auto cur = root;
while(!q.empty()){
cur = q.front();
res.push_back(cur->val);
q.pop();
if(cur->left)
q.push(cur->left);
if(cur->right)
q.push(cur->right);
}
return res;
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探