剑指offer刷题记录
一、贪心题
1、剪绳子
给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],…,k[m]。请问k[0]xk[1]x…xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
解题:
此题为贪心题,每次减掉3,最后剩下的有三种情况:
**情况1:**为0时,n%3==0,最大乘积为pow(3, n/3)
**情况2:**为1时,n%3==1,最大乘积为 pow(3, n/3-1) * 4 // 如n=10,余数为1,3 * 3 * 3 * 1 < 3 * 3 * 4
**情况3:**为2时,n%3==2,最大乘积为pow(3, n/3) * 2
int cutRope(int number) {
if(number == 2 || number == 3)
return number-1;
int res = number/3;
int m = number % 3;
if(m == 0)
return pow(3,res);
else if(m == 1)
return pow(3,res-1) * 4;
else if(m == 2)
return pow(3,res) * 2;
}
2、两两配对(腾讯笔试)
小Q有M(M为偶数)名员工, 第i名员工完成工作的时候有一个拖延时间值ti。现在小Q手里有M/2份工作需要完成, 每一份工作都需要安排两名员工参与, 对于第i份工作所需完成的时间为两名员工的拖延时间值总和。
现在M/2份工作同时开始进行,小Q希望所有工作结束的时间尽量早, 请你帮小Q设计一个优秀的员工分配方案,使得用尽量少的时间完成所有工作,并输出工作所需的最短时间。
示例1
输入
3
1 8
2 5
1 2
输出
10
说明
拖延值为8的和拖延值为2的组队,两名拖延值为5的组队,所以完成工作的时间为10,这是时间最短的方案。
【解题思路】
贪心。最大的和最小的组合,次大的和次小的组合。
#include<bits/stdc++.h>
using namespace std;
struct node {
int cnt;
int time;
}pe[100005];
bool compare(node a, node b) {
return a.time < b.time;
}
int main()
{
int t;
cin >> t;
for(int i = 0; i < t; i++) {
cin >> pe[i].cnt >> pe[i].time;
}
sort(pe,pe+t,compare);
int res = 0;
int left = 0, right = t-1;
while(left <= right) {
int number = min(pe[left].cnt,pe[right].cnt);
number /= (left == right) ? 2 : 1; //若left == right,number折半
res = max(res,pe[left].time+pe[right].time);
pe[left].cnt -= number;
pe[right].cnt -= number;
left += (pe[left].cnt == 0) ? 1 : 0;
right += (pe[right].cnt == 0) ? -1 : 0;
}
cout << res << endl;
return 0;
}
二、递归题
1、机器人的运动范围
地上有一个m行和n列的方格。一个机器人从座标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行座标和列座标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
解题:
此题为典型递归题,利用dfs深度搜索,从起点0,0开始,此解妙处在于利用一维数组flag记录访问状态,每次搜索时求得座标(i , j)在一维数组表示的值。index = i*cols+j; cols 为列值。还有就是单独写个函数,用来求行列的位数之和并返回。
int flag[10000];
int movingCount(int threshold, int rows, int cols)
{
memset(flag,0,sizeof(flag));
int res = dfs(0,0,threshold,rows,cols);
return res;
}
int dfs(int i,int j,int thre,int rows,int cols) {
int index = i*cols+j; //用一维数组简化访问状态
if(flag[index] || i < 0 || i >= rows || j < 0 || j >= cols ||
(num(i)+num(j) > thre))
return 0;
flag[index] = 1;
//判断上、下、左、右是否满足移动
return dfs(i,j-1,thre,rows,cols) +
dfs(i,j+1,thre,rows,cols) +
dfs(i-1,j,thre,rows,cols) +
dfs(i+1,j,thre,rows,cols) + 1;
}
//计算行与列座标的位数之和
int num(int n) {
int sum = 0;
while(n) {
sum += n%10;
n /= 10;
}
return sum;
}
三、回溯题
1、矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如
a b c e
s f c s
a d e e
矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
题解:
此题利用递归+回溯即可求解。这里我是选择使用c++解题的,所以hasPath传参为指针,其实也就是一维数组,这里可以根据座标(i , j)求得matrix中的位置,因此首先求得matrix的长度,然后初始化一个访问状态flag数组,用来记录是否访问过某个座标。此题需要寻得str路径,那么此题可以直接遍历rows,cols中的所有座标。
bool hasPath(char* matrix, int rows, int cols, char* str)
{
int n = strlen(matrix);
int flag[n+5];
memset(flag,0,sizeof(flag));
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
if(judgePath(matrix,i,j,rows,cols,str,flag,0)){
return true;
}
}
}
return false;
}
bool judgePath(char* matrix, int i, int j, int rows, int cols, char* str,
int flag[], int k) {
int index = i * cols + j; //求得二维座标在一维中的index
if(i < 0 || i >= rows || j < 0 || j >= cols ||
matrix[index] != str[k] || flag[index] == 1)
return false;
flag[index] = 1;
if(k == strlen(str)-1) //当匹配到str中最后一位时,匹配成功
return true;
//判断上、下、左、右是否满足
if(judgePath(matrix,i-1,j,rows,cols,str,flag,k+1) ||
judgePath(matrix,i+1,j,rows,cols,str,flag,k+1) ||
judgePath(matrix,i,j-1,rows,cols,str,flag,k+1) ||
judgePath(matrix,i,j+1,rows,cols,str,flag,k+1)) {
return true;
}
//此处需要更改状态,恰是回溯最重要的一步,因为起点可以为任何一个点
flag[index] = 0;
return false;
}
四、队列题
1、滑动窗口的最大值
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
解题:
方法1:最直接方法,两个循环模拟窗口判断
vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
vector<int> res;
int len = num.size();
if(len < size || size == 0) //若数组长度小于窗口大小,或窗口大小为0,返回空res
return res;
for(int i = 0; i <= len-size; i++) { //滑动窗口个数
int maxn = num[i];
for(int j = i+1; j < i+size; j++) { //滑动窗口值的比较
if(maxn < num[j])
maxn = num[j];
}
res.push_back(maxn);
}
return res;
}
方法2:双向队列
vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
vector<int> res;
deque<int> q;
int len = num.size();
if(len < size || size == 0) //若数组长度小于窗口大小,或窗口大小为0,返回空res
return res;
for(int i = 0; i < len; i++) {
//从后面依次弹出队列中比当前num值小的元素,同时也能保证队列首元素为当前窗口最大值下标
while(q.size() && num[q.back()] <= num[i])
q.pop_back();
//队首元素已不再是新窗口的最大值,需弹出
while(q.size() && i-q.front()+1 > size)
q.pop_front();
q.push_back(i);
if(i+1 >= size) //当够窗口大小时,即可添加值了
res.push_back(num[q.front()]);
}
return res;
}
五、数组题
1、数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
解题:
定义一个数组,没插入一次进行一次排序,获取中位数时利用数组长度判断一下
vector<int> nums;
void Insert(int num)
{
nums.push_back(num);
sort(nums.begin(), nums.end());
}
double GetMedian()
{
int len = nums.size();
if(len % 2 == 1)
return nums[len/2]*1.0;
return (nums[len/2]+nums[len/2-1])/2.0;
}
2、表示数值的字符串
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示数值。 但是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。
解题:
简单模拟
bool isNumeric(char* string)
{
// 标记符号、小数点、e是否出现过
bool sign = false, decimal = false, isE = false;
for(int i = 0; i < strlen(string); i++) {
if(string[i] == 'e' || string[i] == 'E') {
if(i == strlen(string)-1)
return false; // e后面一定要接数字
if(isE)
return false; // e不能出现两次
isE = true;
} else if(string[i] == '+' || string[i] == '-') {
// 第二次出现+-符号,则必须紧接在e之后
if(sign && string[i-1] != 'e' && string[i-1] != 'E')
return false;
// 第一次出现+-符号,且不是在字符串开头,则也必须紧接在e之后
if(!sign && i > 0 && string[i-1] != 'e' && string[i-1] != 'E')
return false;
sign = true;
} else if(string[i] == '.') {
// e后面不能接小数点,小数点不能出现两次
if(isE || decimal)
return false;
decimal = true;
} else if (string[i] < '0' || string[i] > '9') // 不合法字符
return false;
}
return true;
}
3、字符流中第一个不相等的字符
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
如果当前字符流没有存在出现一次的字符,返回#字符。
解题:
利用一个int型数组表示256个字符,这个数组初值置为0,遍历已给的字符串,计算出每个字符出现的次数,返回结果时直接从前往后遍历,返回第一个统计值为1的字符
string str;
int flag[256];
//插入一个字符
void Insert(char ch)
{
str += ch;
flag[ch]++;
}
//返回字符串中第一个出现一次的字符
char FirstAppearingOnce()
{
for(int i = 0; i < str.size(); i++) {
if(flag[str[i]] == 1) {
return str[i];
}
}
return '#';
}
4、构建乘积数组
给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2];)
解题:
题目读了好久才读懂的。题目意思可以简化成一个矩阵,左对角线上的值为除了该元素的其他值的乘积
那么改题的解可以理解为:先累乘下三角行的乘积,然后累乘上三角行的乘积,最后返回最终求的结果即可
vector<int> multiply(const vector<int>& A) {
int len = A.size();
vector<int> res(len); //累乘结果
res[0] = 1;
for(int i = 1; i < len; i++) { //下三角行的乘积,累乘到res[i]行中
res[i] = res[i-1]*A[i-1];
}
int t = 1; //累乘变量
for(int i = len-2; i >= 0; i--) { //上三角行的乘积,累乘到res[i]行中
t *= A[i+1];
res[i] *= t;
}
return res; //返回结果
}
5、数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解题:
遍历数组时,利用map统计数字的次数,若大于1,返回结果
bool duplicate(int numbers[], int length, int* duplication) {
map<int,int> mp;
for(int i = 0; i < length; i++) {
mp[numbers[i]]++;
if(mp[numbers[i]] > 1) {
*duplication = numbers[i];
return true;
}
}
return false;
}
6、把字符串转换为整数
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0
输入描述:
输入一个字符串,包括数字字母符号,可以为空
输出描述:
如果是合法的数值表达则返回该数字,否则返回0
示例1
输入
+2147483647
1a33
输出
2147483647
0
解题:
模拟题,注意边界处理
int StrToInt(string str) {
int len = str.size();
if(len == 0)
return 0;
int sum = 0,i;
if(str[0] == '+' || str[0] == '-')
i = 1;
else
i = 0;
for(; i < len; i++) {
if(!(str[i] >= '0' && str[i] <= '9'))
return 0;
else {
if(str[0] != '-' && (INT_MAX - (sum*10)) < (str[i] - '0'))
return 0;
if((INT_MAX - (sum*10)) < (str[i] - '1'))
return 0;
sum = sum*10 + (str[i] - '0');
}
}
if(str[0] == '+' || (str[0] >= '0' && str[0] <= '9'))
return sum;
else
return sum * -1;
}
六、二叉树
1、二叉搜索树的第k个结点
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
解题:
此题利用二叉搜索树的性质,使用中序遍历结果是有序的,中序遍历每一个结点存入数组,最后返回第k个结点即可,注意边界的处理
vector<TreeNode*> res; //存放二叉搜索树的每一个结点
TreeNode* KthNode(TreeNode* pRoot, int k)
{
if(pRoot == NULL || k <= 0) //若二叉树为空,或k<=0时返回NULL
return NULL;
midVisit(pRoot); //中序遍历二叉树
int len = res.size();
if(k > len) //若第K个结点不存在,返回NULL
return NULL;
return res[k-1]; //下标从0开始,因此返回k-1
}
void midVisit(TreeNode* root){
if(root == NULL)
return;
midVisit(root->left);
res.push_back(root);
midVisit(root->right);
}
2、把二叉树打印成多行
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
解题:
层次遍历二叉树,利用队列先进先出性质,按顺序访问每一层的每一个结点
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int> > res;
if(pRoot == NULL)
return res;
queue<TreeNode*> que;
que.push(pRoot);
while(!que.empty()) {
int n = que.size();
vector<int> de;
for(int i = 0; i < n; i++) {
TreeNode* root = que.front();
que.pop();
de.push_back(root->val);
if(root->left != NULL)
que.push(root->left);
if(root->right != NULL)
que.push(root->right);
}
res.push_back(de);
}
return res;
}
3、按Z字形顺序打印二叉树
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
解题:
与层次遍历二叉树相似,利用队列先进先出性质,按顺序访问每一层的每一个结点。不同的是每隔一层需要反转后再加入结果集。
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int> > res;
if(pRoot == NULL)
return res;
queue<TreeNode*> que;
que.push(pRoot);
int flag = 1; // z字形遍历标记
while(!que.empty()) {
int n = que.size();
vector<int> de;
for(int i = 0; i < n; i++) {
TreeNode* root = que.front();
que.pop();
de.push_back(root->val);
if(root->left != NULL)
que.push(root->left);
if(root->right != NULL)
que.push(root->right);
}
if(flag%2 == 1) //模2为1时直接添加,为0时需要先反转后再加入res
res.push_back(de);
else
reverse(de.begin(),de.end()),res.push_back(de);
flag++;
}
return res;
}
4、对称的二叉树
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
解题:
利用递归方法,首先根节点以及其左右子树,左子树的左子树和右子树的右子树比较,左子树的右子树和右子树的左子树比较:
情况1:若左右同时为NULL,返回true
情况2:若左右有一个为NULL,返回false
情况3:若左右值不相等,返回false
bool isSymmetrical(TreeNode* pRoot)
{
if(pRoot == NULL)
return true;
return judge(pRoot->left,pRoot->right);
}
bool judge(TreeNode* left, TreeNode* right) {
if(left == NULL && right == NULL) //若左右同时为NULL,返回true
return true;
if(left == NULL || right == NULL) //若左右有一个为NULL,返回false
return false;
if(left->val != right->val) //若左右值不相等,返回false
return false;
//递归:左子树的左子树和右子树的右子树比较,左子树的右子树和右子树的左子树比较
return judge(left->left,right->right) && judge(left->right,right->left);
}
5、二叉树的下一个结点
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
解题:
当节点为空时,直接返回
当右节点存在时,返回右节点的最左结点
当右节点不存在时,向上寻找公共父节点
TreeLinkNode* GetNext(TreeLinkNode* pNode)
{
if(pNode == NULL)
return NULL;
if(pNode->right != NULL)
{
pNode = pNode->right;
while(pNode->left != NULL) {
pNode = pNode->left;
}
return pNode;
}
while(pNode->next != NULL) {
TreeLinkNode* node = pNode->next;
if(pNode == node->left)
return node;
pNode = pNode->next;
}
return NULL;
}
七、链表题
1、删除链表中的重复结点
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
解题:
建立头结点,以方便碰到第一个与第二个节点就相同的情况;
设置 pre ,last 指针, pre指针指向当前确定不重复的那个节点,而last指针用于往后遍历判断;
ListNode* deleteDuplication(ListNode* pHead)
{
if(pHead == NULL || pHead->next == NULL)
return pHead;
ListNode* head = new ListNode(0); //建立头结点
head->next = pHead;
ListNode* pre = head; //当前确定不重复结点,用于直接指向
ListNode* last = pHead; //用于遍历判断的结点
while(last != NULL) {
if(last->next != NULL && last->val == last->next->val) {
while(last->next != NULL && last->val == last->next->val) {
last = last->next;
}
pre->next = last->next;
last = last->next;
} else {
pre = pre->next; //改变当前确定不重复结点
last = last->next;
}
}
return head->next; //返回结果为头结点的下一个结点
}
2、链表中环的入口结点
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
解题:
利用环的特性,建立快慢指针:fast、slow, fast每次移动两个结点,slow每次移动一个结点,若链表存在环,则fast与last指针一定会相遇,此时任意一个结点回到头结点(如slow=pHead),然后再与fast指针并进,每次移动一个结点,直到指针相遇时,即是环的最终入口。
大伙们只需在纸上画一下模拟一遍就能很快的理解了。
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
if(pHead == NULL || pHead->next == NULL || pHead->next->next == NULL)
return NULL;
ListNode *fast = pHead->next->next, *slow = pHead->next;
while(fast != slow) {
if(fast->next == NULL || fast->next->next == NULL)
return NULL;
fast = fast->next->next;
slow = slow->next;
}
slow = pHead;
while(fast != slow) {
fast = fast->next;
slow = slow->next;c
}
return fast;
}
后序会不断更新……
6月已结束,7月加油!