本仓库链接:https://github.com/guyongpu/SwordOffer.git
本仓库为牛客网剑指offer题目的刷题记录,网址为:https://www.nowcoder.com/ta/coding-interviews ,点击跳转到剑指offer
每个题的代码放在src中,在main.cpp引入对应类的头文件,然后运行测试函数即可,例如需要运行题1时,对main.cpp写成如下形式即可:
#include <iostream>
#include "src/P01_2d_array_find.h"
using namespace std;
int main() {
P01_2d_array_find G;
G.test();
return 0;
}
然后点击运行即可。
题号 | 题目名 | 完成日期 | 描述 | 备注 |
---|---|---|---|---|
P01 | 二维数组的查找 | 2019.9.10 | 问题:在一个递增的二维数组中查找一个数是否存在。 思路:从左上角或者右上角开始查找 |
思考递减数组 |
P02 | 替换空格 | 2019.9.10 | 问题:将空格替换为%20。 思路:从后往前替换,避免移动,时间复杂度O(n) |
小换大,从后往前 大换小,从前往后 |
P03 | 从尾到头打印链表 | 2019.9.10 | 问题:将链表从尾到头存放到vector中 思路:顺序遍历,然后对vector进行反转 |
不建议反转链表,会改变链表结构 |
P04 | 重建二叉树 | 2019.9.10 | 问题:根据前序和中序递归构建二叉树 思路:递归构建二叉树 |
复习时要回顾这个题 |
P05 | 用两个栈实现队列 | 2019.9.11 | 问题:用两个栈实现队列的push和pop操作 思路:push时,直接存入stack1;pop时,若stack2不为空,则直接弹出栈顶元素,否则,将stack1放入stack2,再弹出栈顶元素。 |
无 |
P06 | 旋转数组的最小数字 | 2019.9.11 | 问题:数组旋转一次后,找出数组的最小值 思路:用双指针的二分查找方法 |
不要顺序遍历 |
P07 | 斐波那契数列 | 2019.9.11 | 问题:输出斐波那契数列的第n项 思路:递归计算时间空间复杂度过大,用非递归做,F(n)=F(n-1)+F(n-2) |
用非递归方法 |
P08 | 跳台阶 | 2019.9.11 | 问题:计算跳上number级台阶的跳法数量 思路:递归计算时间空间复杂度过大,用非递归做,F(n)=F(n-1)+F(n-2) |
用非递归方法 |
P09 | 变态跳台阶 | 2019.9.11 | 问题:计算跳上number级台阶的跳法数量,每次可以选择跳1-n级 思路:利用数学归纳法推导,F(n)=2*F(n-1) |
用非递归方法 |
P10 | 矩形覆盖 | 2019.9.11 | 问题:用2*1的矩形去覆盖2*number的大矩形,计算覆盖的方法数 思路:这个题也是斐波拉契数列,F(n)=F(n-1)+F(n-2) |
用非递归方法 |
P11 | 二进制中1的个数 | 2019.9.11 | 问题:给定一个整数,计算其二进制中1的个数 思路:介绍了3种方法,关键是第2和第3种。方法1,采用每次右移n,提前处理负数;方法2采用每次将flag左移;方法3采用n=n&(n-1)的方式计算 |
掌握3种方法 |
P12 | 数值的整数次方 | 2019.9.12 | 问题:不用库函数,计算base的exponent次方 思路:考虑两个点,一是int的绝对值算法,考虑边界值,使用unsigned int处理;二是0的处理,返回0 |
绝对值的处理 |
P13 | 调整数组顺序使奇数位于偶数前面 | 2019.9.12 | 问题:将一个数组的奇数放在前面,偶数放在后面,奇数之间、偶数之间的相对位置不变 思路:可以采用双指针平移的做法,排序的做法,用两个vector组合做法都可以 |
如果只要求奇数前,偶数后,则用双指针做法 |
P14 | 链表中倒数第k个结点 | 2019.9.13 | 问题:输出该链表中倒数第k个结点 思路:利用双指针思想,指针1先走k步,之后指针2与指针1同步移动,直到链表结尾 |
考虑0,空链表,倒数第x个,倒数第1个,倒数第length个等情况 |
P15 | 反转链表 | 2019.9.13 | 描述:将链表进行反转后,然后输出新链表的表头 思路:使用三个指针,分别指向前,中,后三个位置,中表示正在处理的节点 |
常见题,注意复习 |
P16 | 合并两个排序的链表 | 2019.9.13 | 描述:将两个有序链表合并成一个有序链表 思路:使用一个指针,先构建头结点,然后再逐个构建起一条链表,类似于新建链表的过程 |
常见题,注意多复习 用非递归实现的,而书上使用递归,但本质是一样的 |
P17 | 树的子结构 | 2019.9.13 | 描述:第一步,先在树A中找到一个和树B根节点的值相等的节点R;第二步,判断树A中以R为根节点的子树是不是包含树B一样的结构。 HasSubtree,找到一个相等的节点;DoesTree1HaveTree2,开始计算两个树是否包含。 | 常见题,注意复习 |
P18 | 镜像二叉树 | 2019.9.13 | 描述:给定一个的二叉树,将其变换为原二叉树的镜像 思路:将树的左右子树进行交换,然后再分别对左右子树进行镜像处理。 |
常见题,注意复习 |
P19 | 顺时针打印矩阵 | 2019.9.13 | 描述:将一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字 思路:第一步:先打印整圈的,圈数是行列的最小值minNum/2;第二步,如果minNum是奇数,说明有单独的行/列,当行>列,则有列,从上到下打印,当列>=行,则有行,从左到右打印。 |
1.圈数是行列的最小值minNum/2; 2.minNum是奇数则打印单独行/列。 |
P20 | 包含min函数的栈 | 2019.9.14 | 描述:实现一个能够得到栈中所含最小元素的min函数 思路:本题目需要使用两个栈,数据栈和辅助栈。辅助栈只在压栈元素比栈顶元素小或等于才入栈,出栈则与辅助栈顶元素相等才出栈 |
入栈时与辅助栈栈顶相等也要入栈 |
P21 | 栈的压入、弹出序列 | 2019.9.14 | 描述:判断一个序列是否可能为一个栈的弹出顺序 思路:遍历pushV[i],如果遇到popV[cnt]相等,先压栈,然后popV[cnt]与栈中逐个比较,否则直接将pushV[i]压入栈中当cnt与popV的size相等时,则说明匹配成功,否则匹配失败。 |
比较阶段,结束判断条件 |
P22 | 从上往下打印二叉树 | 2019.9.14 | 描述:从上往下打印出二叉树的每个节点,同层节点从左至右打印。 思路:本题书上的方法是采用先进先出队列,每遍历一个节点,则把该节点的左右节点放到队尾,知道队为空,则结束。本人做法原理是一样的,只是用的vector实现,vectree存放本轮遍历的节点,一轮完成后,清空vectree,再把其子节点加入到vectree中,然后进行下一轮遍历,直到vectree的size为0,则说明所有节点已遍历。 |
二叉树的层序遍历,用队列更好 |
P23 | 二叉搜索树的后序遍历序列 | 2019.9.14 | 描述:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。 思路:后序遍历的最后一个元素是根,则序列必须可以分为左右两部分,左部分<root,右部分>root,不满足则返回false,满足则继续进行递归。递归结束条件:每一个sequence只有一个元素,则返回true. |
常见题,注意多复习 输入空树返回false. |
P24 | 二叉树中和为某一值的路径 | 2019.9.14 | 描述:打印出二叉树中结点值的和为输入整数的所有路径,终点为叶子节点,按长度降序 思路:按左右子树进行递归,当和相等且为叶子节点,则说明找到一条路径Path,然后将Path存入result中,如果不符合要求,则先将当前节点加入Path中,然后分别对左、右子树递归。 |
题目不难,但要复习 |
P25 | 复杂链表的复制 | 2019.9.14 | 描述:输入一个复杂链表,返回结果为复制后复杂链表的head 思路:本题分为3步。1.复制节点,并插入到原链表中,如A->A'->B->B'->C->C';2.设置random指针的值;3.拆分原链表和新链表。 |
思路比较巧妙,注意不能破坏旧链表结构 |
P26 | 二叉搜索树与双向链表 | 2019.9.14 | 描述:将二叉搜索树转换成一个排序的双向链表 思路:二叉搜索树进行中序遍历恰好是排序的结果,借用一个指向当前链表最后一个节点的指针来构建链表 |
中序遍历思想,第一次不会做,要复习 |
P27 | 字符串排列 | 2019.9.15 | 描述:输入一个字符串,按字典序打印出该字符串中字符的所有排列。 思路:将字符串分为两部分,第一个字符、其后面的所有字符。然后用第一个字符和后面的所有字符逐个交换。 |
注意复习 |
P28 | 数组中出现次数超过一半的数字 | 2019.9.15 | 描述:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字,不存在则输出0. 思路:采用阵地攻守的思想。第一个数字作为第一个士兵,守阵地;count = 1;遇到相同元素,count++; 遇到不相同元素,即为敌人,同归于尽,count--;当遇到count为0的情况,又以新的i值作为守阵地的士兵,遍历到最后还留在阵地上的士兵,有可能是主元素。再加一次循环,记录这个士兵的个数看是否大于数组一半即可。 |
常见题,要复习 |
P29 | 最小的K个数 | 2019.9.15 | 描述:输入n个整数,找出其中最小的K个数 思路:利用大顶堆做即可,思路比较简单,先构造一个K大小的大顶堆,然后逐个对根比较,如果input[i]比根小,则替换根,重新构建,主要涉及堆的make、pop、push和sort等操作,O(n*logK) |
常见题,要复习 |
P30 | 连续子数组的最大和 | 2019.9.16 | 描述:计算一个数组的最大连续子序列的和 思路:常规解法,逐个累加,当tmp_sum<0,则替换掉,每次与sum比较大小 |
常规解法 动态规划 |
P31 | 整数中1出现的次数 | 2019.9.16 | 描述:从1到整数N中1出现的次数 思路:逐位思考,可以计算1~9出现的次数,修改x即可,0除外 |
注意复习 |
P32 | 把数组排成最小的数 | 2019.9.16 | 描述:输入一个正整数数组,把数组里所有数字拼接起来排成一个最小的数 思路:主要运用到一个字符串排序,当str_ab< str_ba时,a在前,否则b在前 |
注意复习 |
P33 | 丑数 | 2019.9.16 | 描述:把只包含质因子2、3和5的数称作丑数,找第N个丑数。 思路:维护三个指针,每次找三个指针对应的乘积的最小值加入队列,同时进行移动指针。 |
常规解法和优化解法 |
P34 | 第一个只出现一次的字符 | 2016.9.16 | 描述:在一个字符串中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回-1 思路:可以采用构造hash表的思想,即构造pos[52],int cnt[52],遍历两次即可求解。也可以采用map思想,直接strmap<char,int>,做法也比较简单 |
hash表 map方法 |
P35 | 数组中的逆序对 | 2019.9.22 | 描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P 思路:归并排序的改进,把数据分成前后两个数组(递归分到每个数组仅有一个数据项),合并数组,合并时,出现前面的数组值array[i]大于后面数组值array[j]时;则前面数组array[i]~array[mid]都是大于array[j]的,count += mid+1 - i。 |
常见题,要复习 |
P36 | 两个链表的第一个公共结点 | 2019.9.23 | 描述:输入两个链表,找出它们的第一个公共结点 思路:由于链表是单向,所以链表交点之后的结点都相同,则可以让长的先走x步,然后再一起走,直到最后相遇。或者通过将两个链表压入两个栈中,然后逐个出栈,开始时都相等,直到遇到第一个不相等的结点。 |
题目比较简单 |
P37 | 数字在排序数组中出现的次数 | 2019.9.23 | 描述:统计一个数字在排序数组中出现的次数。 思路:用2次二分查找,第一次查找K的位置,第二次使用二分查找确定K的起点和终点。或者,在确定好K的位置后,用双指针前后移动,统计数目。 |
data为空判断+降序反转 |
P38 | 二叉树的深度 | 2019.9.23 | 描述:输入一棵二叉树,求该树的深度。 思路:使用层序遍历思想,1.队列实现;2.vector实现;3.递归实现 |
重要题目。还可以改为输出最长的路径结果,结合P24做即可 |
P39 | 平衡二叉树 | 2019.9.23 | 描述:输入一棵二叉树,判断该二叉树是否是平衡二叉树。 思路:法1:利用后序遍历思想,从下往上遍历,某一结点的深度等于其到叶子结点的路径的长度。法2:O(n^2),对每个结点调用求深度的函数,然后判断。 |
重要题目 |
P40 | 数组中只出现一次的数字 | 2019.9.24 | 描述:一个整型数组里除了两个数字之外,其他的数字都出现了两次 思路:采用异或的思想,将所有数异或,得到不等于0的数,然后根据1的位置分为两组异或,然后在两组中获得num1和num2. |
如果只有一个数出现一次,则直接异或即可得到结果。 |
P41 | 和为S的连续正数序列 | 2019.9.24 | 描述:输出和为S的连续正数序列,要求序列内部升序,序列间按起始数字升序. 思路:采用双指针,small=1,big=2,然后逐步将指针往右移,约束条件:small <= sum/2. |
记住方法,另外起始不能为0 |
P42 | 和为S的两个数字 | 2019.9.24 | 描述:在数组中查找两个数,使得他们的和正好是S,要求乘积最小. 思路:从两端开始找,采用双指针做法,一旦找到,就是乘积最小的. |
两个数和为S时,两个数越接近乘积越大。要考虑找不到的情况. |
P43 | 左旋转字符串 | 2019.9.24 | 描述:将一个字符串循环左移n位 思路:法1:采用旋转方式实现 YX = (X^T + Y^T)^T ,通过三次reverse实现;法2:采用普通的substr截取字符串处理. |
reverse第二个参数是下一个元素的地址 |
P44 | 翻转单词顺序列 | 2019.9.24 | 描述:将一个句子的单词顺序进行翻转,单词内部顺序不变 思路:利用YX = (X^T + Y^T)^T的思想,先对每个单词翻转,然后再对整个单词翻转. |
记住YX = (X^T + Y^T)^T方法 |
P45 | 扑克牌顺子 | 2019.9.24 | 描述:给5张牌,判断是否能组成顺子 思路:1.排序;2.统计0的数量;3.遍历非0数字,判断0的数目是否够补位,如果不够输出false,要考虑到对子. |
要仔细. |
P46 | 孩子们的游戏(圆中最后剩下的数) | 2019.9.24 | 描述:一个n大小的约瑟夫环,每次去掉一个数,知道只剩最后一个数即为结果 思路:标准结果和常规解法 |
掌握两种方法 |
P47 | 求1+2+3+...+n | 2019.9.24 | 描述:求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C) 思路:利用与运算的特点,(n>0) && (sum = n + Sum_Solution(n - 1))进行递归运算 |
构造函数法也可求解 |
P48 | 不用加减乘除做加法 | 2019.9.24 | 描述:求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号 思路:对每一位进行讨论计算,使用flag诸位左移,设置sum的位.也可以采用标准解法. |
数据以补码存储 |
P49 | 把字符串转换成整数 | 2019.9.25 | 描述:将一个字符串转换成一个整数的功能,当string不符合数字要求时返回0. 思路:正常思路,注意正负号,非法字符,标记位,空字符串,整数上下溢出. |
注意细节. |
P50 | 数组重复的数字 | 2019.9.26 | 描述:在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,找出一个重复的数字. 思路:逐个遍历,如果numbers[i]!=i,判断numbers[i]与numbers[numbers[i]]是否相等,相等则说明是重复元素,否则numbers[i]与numbers[numbers[i]]交换位置. |
n个数字,每个数组0~n-1之间,无此条件会越界,另外检查合法性. |
P51 | 构建乘积数组 | 2019.9.26 | 描述:给定数组A[0,1,...,n-1],构建数组B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1],不使用除法. 思路:如果直接计算时间复杂度为O(n^2),考虑将B[i]=array1[i]*array2[i],其中array1[i]=A[0]*...*A[i-1];而array2[i] = A[i+1]*...*A[len-1] |
如果可以使用除法,则B[i]=II(j=0->n-1)A[i] / A[i],时间复杂度为O(n). |
P52 | 正则表达式匹配 | 2019.9.26 | 描述:实现一个函数用来匹配包括'.'和'*'的正则表达式. 思路:第1步,先处理*,若能匹配,则三种情况:(1)跳过x*,即matchCore(str,pattern+2);(2)匹配x*,即matchCore(str+1,pattern+2);(3)匹配下一个str,即matchCore(str+1,pattern);若不能匹配,则忽略跳过x*。第2步,然后处理正常字符的情况。 |
常见题 |
P53 | 表示数值的字符串 | 2019.9.26 | 描述:请实现一个函数用来判断字符串是否表示数值(包括整数和小数). 思路:直接判断,以E为界限,分为E的左边和E的右边,E的左边为正常数值,E的右边为整数. |
仔细判断,也可考虑用正则表达式匹配. |
P54 | 字符流中第一个不重复的字符 | 2019.9.26 | 描述:请实现一个函数用来找出字符流中第一个只出现一次的字符. 思路:使用 HashTable[256]大小的哈希表,初始值设置为-1,之后值为第一次出现的index,重复出现时设置为-2,然后输出当前HashTable[256]中最小的非负数对应的字符. |
字符表考虑256大小的哈希表 |
P55 | 链表中环的入口结点 | 2019.9.26 | 描述:给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null. 思路:第1步,判断链表是否成环,快慢指针相遇;第2步,pFast先走step步,然后再一起走,首次相遇就是入口. |
使用链表注意细节 |
P56 | 删除链表中重复的结点 | 2019.9.27 | 描述:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针 思路:1.处理头结点 2.处理其他结点。使用双指针做法,pPreNode 指向不重复的结点,检测pNode和pNode->next是否重复,如重复,找到 pNext,使 pPreNode指向pNext. |
保留和不保留重复节点两个版本,考虑全/不/头/尾/部分重复,单/空链表. |
P57 | 二叉树的下一个结点 | 2019.9.27 | 描述:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回. 思路:分3种情况。1.pNode是父结点的左结点,则直接返回父结点;2.pNode是父结点的右节点,则一直向上找,直到pFFNode->left = pFNode,返回pFFNode;3.如果有右子树,则一直找最左结点. |
考虑父结点为空. |
P58 | 对称的二叉树 | 2019.9.27 | 描述:请实现一个函数,用来判断一颗二叉树是不是对称的. 思路:二叉树左右节点递归比较,左节点与右节点比较,同为空或者val值相等。 |
注意复习 |
P59 | 按之字形顺序打印二叉树 | 2019.9.27 | 描述:请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,其他行以此类推。 思路:这是二叉树层序遍历的变式题,只需在打印是设置order控制打印顺序即可,其他和层序遍历是一样的。 |
复习层序遍历 |
P60 | 把二叉树打印成多行 | 2019.9.27 | 描述:从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。 思路:二叉树的层序遍历 |
掌握二叉树遍历方法 |
P61 | 序列号二叉树 | 2019.9.28 | 描述:实现二叉树的序列化,并实现二叉树的反序列化. 思路:序列化采用先序遍历完成,反序列化遍历字符串,逐步重建树. |
此题比较麻烦,记得复习 |
P62 | 二叉搜索树的第k个结点 | 2019.9.28 | 描述:给定一棵二叉搜索树,请找出其中的第k小的结点. 思路:采用中序遍历做,其次,k要使用引用,否则k不会发生变化,当k等于0返回;当k小于0时停止递归. |
备注:注意复习 |
P63 | 数据流中的中位数 | 2019.9.28 | 描述:输入一串数字流,得到其中的中位数. 思路:采用最大最小堆做效率最高,插入复杂度O(logn),获取中位数O(1) |
也可以采用插入排序思想,或者快排Partition,排序链表,ALV树 |
P64 | 滑动窗口的最大值 | 2019.9.28 | 描述:给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值. 思路:采用双端队列来做,队首始终是当前窗口的最大值. |
学习该方法 |
P65 | 矩阵中的路径 | 2019.9.28 | 描述:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径. 思路:测试所有可能的起点,然后使用递归回溯算法,visited用于标记是否访问,在匹配失败是进行回退. |
在匹配失败要进行回退 |
P66 | 机器人的运动范围 | 2019.9.28 | 描述:一个m行n列的方格矩阵,机器人从(0,0)开始走,不能进入行坐标和列坐标的数位之和大于k的格子,机器人最多可到达多少个格子. 思路:使用visied做标记,访问过为true,未访问过为false,然后使用递归回溯计算. |
递归回溯典型例题 |
P67 | 剪绳子 | 2019.9.28 | 描述:一根长度为n的绳子,请把绳子剪成m段,记为k[0]、k[1]、...k[m],请问k[0]xk[1]x...xk[m]可能的最大乘积. 思路:采用动态规划或者贪心算法均可求解. |
两种方法都需要掌握 |
本次是集中式的刷题,自2019.9.10早开始,于2019.9.28晚完成,其中9.17-9.21忙于着手实现C++服务器和搭建个人主页及博客,因此该段时间没有刷题。
以前只听说这本书,一直没完整做一遍,这次一次性刷完后,感觉里面的题目还是有很多经典题目,比如树的遍历、镜像树、对称树、双指针、链表成环、字符统计、partition的使用等等,而且大多数是面试中常见的问题,因此还是需要复习的。
接下来就是开始刷LeetCode了,毕竟是和剑指offer齐名的编程题集,不过不会像这次这么全天集中式地刷了,一边学习专业知识一边刷题。
于2019年9月28日21:56:00 记