剑指Offer刷题

leetcode 链接

3、数组中重复的数字 code
可以用set存储出现过的数字来判断,但是空间复杂度较高。
遍历每一个元素nums[i],当nums[i] != i时,需要将nums[i]放到它正确的位置上,即将nums[i]和nums[nums[i]]进行交换。这样,遍历过的位置都是正确的元素。当某个元素不在自己应在的位置,且应在的位置已经有相同元素时,可以断定该元素即重复元素。注意在python的实现中交换时的顺序不能换,原因仍未知。
leetcode 287也可以用该方法解决。code_287
4、 二维数组中的查找 code
注意到以数组右上角元素为根,可形成一个二叉查找树。然后小于当前元素向左查询,大于向右查询。
7、 重建二叉树
首先想到的是直接用给定的函数递归:code1

def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:

遇到了下标越界的问题,原因是构建左子树时需要考虑当前数组根节点是否已无左子树(根节点在中序数组为最左元素),同理构建右子树时需要考虑已无右子树(根节点在中序数组为最右元素)。
这种方法时间和空间效率都较低,因为直接使用原函数递归时,生成了很多新的子数组。
之后改为以下做法:code2
首先用字典记录中序数组每个元素的下标,这样后面就不需要在中序数组中遍历查找根节点的位置。
在给定函数内定义递归函数,递归函数的参数为 root,left,right,分别表示根节点在前序数组的位置,以及当前子树在中序数组中的左右下标。
11、旋转数组中的最小值 code
数组中可能包含重复元素。通过将 mid 位置元素和最左元素 nums[0] 比较,大于 nums[0] 则说明 mid 在最小元素左边,可以令 left = mid+1;小于 nums[0] 说明在最小元素位置或其右侧,可以令 right = mid;
若 nums[mid] 等于 nums[0],则无法判断 mid 位于最小元素左侧还是右侧,直接遍历从 left 到 right 所有值求得最小值。
类似题目:code
数组中不包含重复元素,则不需要考虑 nums[mid] 等于 nums[0] 的情况。
18、删除链表的节点 code
注意删除首节点的特殊情况。
40、最小的k个数 code
利用 heapq 实现优先队列。默认是最小堆,可通过添加 -x 来实现最大堆。
堆初始化:heapq.heapify(max_q)
堆插入元素:heapq.heappush(maxq, -x)
删除堆顶元素:heapq.heappop(maxq)
26、树的子结构 code
判断树B是否为A的一个子结构(和子树不同,B可以缺掉一些分支)
首先先序遍历A的各个节点Ai(通过给定函数 isSubStructure 的递归实现),再判断B是否为为Ai的同根子结构(通过 compare 函数递归实现)。
30、包含min函数的栈 code
定义一个 min_stack,只存放递减的元素。
12、矩阵中的路径 code
利用 dfs 搜索,注意在未搜索成功返回 False 之前要把 marked 赋值为 False,因为其它路径还需要访问。
13、0~n-1中的缺失数字 code
用二分法搜索确实后的第一个位置,注意确实最后一个数字的情况。
57、和为s的两个数字 code
在递增数组中,找到和为s的两个数字。
开始使用二分查找法,遍历数组的元素i,在i之后的数组中通过二分查找搜索target-i。
结果超时,该方法时间复杂度为O(nlogn)。
可以使用字典记录遍历过的元素,可以将时间复杂度降至O(n)。
较好的解法应该是双指针法:
初始化:i=0 j=n-1
令nums[i]+nums[j]和target比较,若小于则令i加一,等于则返回结果,大于则令j减一。
证明:当和小于target时,令i加一,相当于删除了 [i,i+1],[i,i+2],...,[i,j-1] 这几种组合,而这些组合的和小于target,因此可以删去。当和大于target时同理。
问题:
当和小于target时,为什么令i加一,而不令j加一?
答:指针j由j+1到j是由于nums[k]+nums[j+1]>target,其中k<=i,因此nums[i]+nums[j+1]>target,即nums[i]和j之后的数相加都大于target,因此只能让i加一。
57、二、和为s的连续正数序列
给定一个整数target,求出所有和为target的连续子序列。
方法一:code1
遍历开始值,用二分查找求是否存在结束值。
时间复杂度:O(nlogn)
方法二:code2
遍历开始值s,根据求和公式可以求得结束值L,若存在大于s的整数解,则将该序列加入结果。
由于最大的连续序列的开始值为 target // 2(例如15的最大连续序列为[7,8]),因此开始值s从0到 target // 2遍历。
时间复杂度:O(n)
方法三:code3
使用滑动窗口。i代表窗口开始值,j代表窗口结束值的下一个值。sum为序列i到j-1的和。
初始状态:
i=j=1, sum=0
当sum等于target,将该序列加入结果;若sum>target,则令sum-i,i+1;若sum<target,则令sum+j,i-1。i和j只增不减。
证明:
类似于上题,
若sum>target,令i加一,则删去了(i,...,j+1), (i,...,j+2)...这些序列。而这些序列和大于target。
此外还删去了(i,...,j-1)等序列。而指针j从j-1到j,是因为序列(k,...,j-1)的和小于target,其中k<=i,因此序列(i,...,j-1)小于target。
55、二叉树的深度
方法一:dfs code1
方法二:层序遍历 code2
层序遍历可用队列实现。也可用两个交替的数组实现(更方便)。
56、数组中数字出现的次数 code
若数组中只有一个不重复出现的数字,可通过异或来得到该数字。
而该题中数组有两个不重复出现的数字a和b,需要首先将数组划分为两个数组,a和b被划分到两个数组,相同的数字被划分到某一个数组。
划分方法:
首先求出所有数字的异或,即c=a^b。若c的某位为1,则说明该位a和b不同,可依此将a和b划分到不同数组。设该位为1的数字对应d。
令res1 = res2 = 0。遍历数组中所有元素,与d相与,结果为0则该元素属于数组1,与res1相与;否则与res2相与。结果即[res1, res2]。
56、 [数组中数字出现的次数](https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof/)
数组中只有一个数字出现一次,其余数字都出现3次,求只出现一次的数字。
使用大小为32的数组cnt统计所有数字二进制表示各位的出现次数,求余3则得到只出现一次的数字的二进制表示。(只有一个数字出现一次,其余数字都出现m次的问题都可以这样解决)
求数字n的二进制:
n与1相与,得到最低位的数字;n右移一位,与1相与,得到第二低位的数字;以此类推。
求二进制的十进制表示:
方法一、[code1](https://leetcode-cn.com/submissions/detail/142578487/)
将res初始化为0,c=cnt[0] % 3 为最高位,res和c<<31位执行或运算,便完成了res最高位的赋值。以此类推。
方法二、code2
将res初始化为0,c=cnt[0] % 3 为最高位,res和c执行或运算,便完成了res最高位的赋值,res左移一位,准备下一位的赋值。以此类推。
35、复杂链表的复制
方法一:code
字典记录每个节点对应的新节点,用递归实现复制。
方法二:code
字典记录每个节点对应的新节点,两遍循环实现复制。
方法三:code
首先将新链表的节点插入到旧链表的间隔,然后遍历整个链表对新链表的random节点进行赋值,最后拆开新旧链表。
31、下一个排列 code
首先从数组后往前找到第一个nums[i] > nums[i-1]的下标i。
这样数组在[i,n)是递减的(即不满足nums[i]>nums[i-1])。
因此数组从i开始已经达到了最大排列,下面需要从后往前找到第一个大于nums[i-1]的下标j,并将nums[i-1]与nums[j]交换。然后由于j是第一个大于nums[i]的元素的下标,因此当 k > j时,nums[k] <= nums[j]。且由于[i,n)是递减的,因此当 k < j 且 k >= i 时,nums[k] > nums[j]。
因此数组在[i,n)仍是递减的,需要将[i,n)的元素倒序翻转,则得到下一个排列。
43、1~n 整数中 1 出现的次数 code
记录4个变量:high, low, digit, cur。
其中cur是当前位的数字,digit是当前位位数(如个位是0,十位是1),high代表当前位左边的数组成的数字,low代表当前位右边的数组成的数字。
从低位到高位依次求每一位出现1的次数,
若cur为0,则在所有可能取值中,当前位为1时,高位需要小于high,而高位小于high时,低位可取任意值。因而高位可取0到high-1共high种可能,低位有10**digit种可能。因此1出现的次数为high * (10 ** digit);
若cur=1,则在所有可能取值中,则当前位为1时,若高位小于high时,低位可取任意值。若高位等于high时,低位只能取0到low共low+1种可能值,因此1出现的次数为high * (10 ** digit) + low + 1;
若cur>1,则在所有可能取值中,则当前位为1时,高位可取0到high共high+1种值,低位可取任意值。1出现的次数为(high+1) * (10 ** digit)。
59、滑动窗口的最大值 code
记录窗口的最大值max_num以及等于最大值的个数max_cnt,窗口滑动时,删去左端元素rm_val,右端增加一元素add_val,若rm_val等于max_num,则max_cnt减一。若max_cnt等于0,则重新计算窗口最大值。add_val和max_cnt对比,若等于max_num,则max_cnt加一。
61、扑克牌中的顺子 code
需要满足两个条件:首先扑克牌中不能有0以外的重复值;非零元素中最大值减去最小值小于5.
36、二叉搜索树与双向链表 code
将二叉搜索树转换为双向链表,节点的左节点指向前继节点,右节点指向后继节点。
中序遍历二叉搜索树,即从小到大遍历。用类遍历 prenode 记录上一个访问的节点,将当前节点与prenode连接。递归遍历时返回最右节点,将最右节点与首节点连接。
44、数字序列中某一位的数字
方法一:code1
逐个数字遍历,超时。时间复杂度O(N)
方法二:code2
按照数量级遍历。设digit位的数字最小为start,则digit位的数字共有9*start个,则共有9*start*digit个数。如2位数最小为10,10~99共有9*10个数,共9*10*2位。
首先确定n对应的位数,然后确定其所在数字,最后确定其所在位的数。时间复杂度:O(log_{10}^N)。
42、连续子数组的最大和
方法一:code1
定义变量sum,遍历数组每一个元素n,sum加上n,令res=max(res, sum)。如果sum<0,则sum不会使后续的和变大,因此令sum=0。
改进:code2
不定义sum,通过nums[i] += nums[i-1]来累计和。只有当nums[i-1]大于0时,才累计。
13、机器人的运动范围
方法一:code1
dfs,每次遍历上下左右四个元素,通过逐数字求和得到数位和,与k对比。
改进:code2
一、从0,0位置开始,只需要向右向下遍历即可遍历到所有元素,不需要遍历左上两个元素。
二、由于m,n<=100,因此从0开始计数行数和列数均小于100,可找到数位和递推规律:
记录行数位和sum1和列数位和sum2,初始化为0。遍历右边元素时,若j+1为10的倍数,则数位和变为sum2-8(如19到20,9到10等),否则sum2变为sum2+1。sum1同理。
46、把数字翻译成字符串
方法一:code1
dfs,每次可访问一个元素,若从当前元素开始的两个元素所组成的数字<=25且当前元素不为0,则还可以访问从当前元素开始的两个元素。
改进:dfs过程中存在重复计算,因此设置dp数组,dp[i]表示从i开始的数字可翻译成的字符串数量,当dfs访问到第i个元素且dp[i]>0时,直接返回dp[i]。
方法二:code2
非递归。设置dp数组,dp[i]表示以i结束的数字可翻译成的字符串数量。当第i-1,i个元素所组成数字可翻译时,dp[i]=dp[i-1]+dp[i-2],否则dp[i]=dp[i-1]。
24、反转链表
方法一:code1
双指针,pre和cur,初始化:pre, cur = None, head。注意如果初始化为head, head.next会繁琐不少。
方法二:code2
递归,当head==None或者head.next==None时,返回head。否则递归求得后续翻转好的链表node,与当前节点连接:(后续翻转好的链表与当前节点连接的方式不容易想到

head.next.next = head 
head.next = None

最后返回node,node指向了初始链表的尾节点。
92、反转链表 II code
用一个新的伪头节点new_head指向第一个节点,这样可以同时处理left>1和left=1的情况。
先遍历到left指向节点的前一节点,记为pre_left_node。然后用双指针翻转left到right的节点,最后连接。
或者首先遍历得到left, right所指向的节点,然后截断链表,将left到right的链表用递归翻转,再连接。
39、数组中出现次数超过一半的数字
方法一:code1
通过字典统计。
方法二:code2
摩尔投票法。定义出现次数超过一半的数字为众数。设数组的众数为x。初始票数vote=0。先假设第一个数字n为x,从该数字开始遍历数组元素,若等于n则vote加一,否则vote减一。
当vote=0时,剩余数组的众数不变。这是因为:
若n==x,则vote=0说明遍历过的数字中一半x,一半非x,剩余数组的众数仍为x。
若n!=x,则vote=0说明遍历过的数字中x小于等于一半,则剩余数组中x更多。
因此,当vote=0时,假设剩余数组第一个元素为x,重复以上过程。由于题目假设众数肯定存在,因此最终假设的众数即所求x。
32、从上到下打印二叉树 III
之字形打印二叉树。
方法一:code1
层序遍历,偶数层的结果倒序。
方法二:code2
双栈。定义奇数层栈p和偶数层栈q。p的pop顺序为从左到右,添加子节点至q时先将左节点入栈,再将右节点入栈。这样q的pop顺序就是从右到左,先将右节点入栈,再将左节点入栈。
方法三:code3
双端队列。与方法二类似。
59、队列的最大值 code
当队列加入一个较大值时,队列中的较小值对求队列的最大值没有意义,因为首部的元素首先移出队列,较小值直到移出队列队列中的最大值一直不是较小值。
维护一个递减的双端队列 max_que,当向队列中添加元素n时需要将 max_que中小于n的值pop出来。
60、n个骰子的点数
方法一:code1
深度优先搜索。时间复杂度 O(6^n),超时。
方法二:code2
动态规划。定义dp数组,dp[i][j]表示i个筛子实现点数为j的可能个数。
初始化:dp[0][1]~dp[0][6]=1。
状态转移方程:
dp[i][j] = \sum_{k=1}^6dp[i-1][j-k]
时间复杂度:O(n^2),空间复杂度O(n^2)
优化空间复杂度:
使用一维dp数组。注意边界条件。
31、栈的压入、弹出序列 code
利用一个栈stack模拟。idx初始指向出栈序列第一个元素。遍历入栈序列,当某元素不等于出栈序列idx所指元素时,将该元素入栈。否则不断出栈,同时idx加一,直至栈顶元素不等于idx所指元素。
16、数值的整数次方
方法一:code1
不断累乘,时间复杂度O(n),超时。
方法二:code2
当n为偶数时,
x^n = (x^2)^{n//2}
当n为奇数时,
x^n = (x^2)^{n//2}x
根据上述公式,可不断将x平方,同时n整除2。用变量res累乘n为奇数时多出的x。最终n等于1时x会被乘到res中,res即最终解。
时间复杂度:O(log_2^n)。
49、丑数 code 参考
第n个丑数肯定是前n-1某个丑数乘以[2,3,5]中的某个数得到的。用三个指针a,b,c分别指示2,3,5已乘过的最大丑数的下一个丑数的下标,则求下一个丑数时,即这三个指针指向的数分别乘以2,3,5得到的数中的最小值。同时判断最小值由[2,3,5]中的哪个乘得,将其指针加一,注意[2,3,5]中的多个数可能得到最小值,因此用if而不用if...else...
20、表示数值的字符串 code 参考
使用三个变量 is_num,is_e,is_dot 分别表示 数字,E或e,小数点是否出现过。该题目较 复杂!!!
29、顺时针打印矩阵 code
从第0层(即最外层)开始向内逐层遍历,每一层依次向右、下、左、上遍历。注意需要判断当向某个方向无元素时,需要跳出循环,否则后面其它方向可能还可以遍历,如:
[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
在第一层(即 6,7),向右可以遍历,但是向下不能遍历,如果不跳出循环的话,后面向左还可以遍历。
31、最近最少未使用缓存(LRU cache) code
使用一个字典,以及一个双向链表来完成。表头为最近使用的,表尾为最久未使用的。
该题的关键在于想到双向链表。开始使用数组存储,put 和 get的时间复杂度都是O(n),导致超时。
85、 生成匹配的括号 code
递归,保证左括号数大于等于右括号数、且小于n。
95、最长公共子序列
注意区分子序列和子串这两个概念。 参考
10、和为 k 的子数组 code
数组中可能包含负数,因此不能用双指针做。
设s[i]为nums[0]到nums[i]的累计和,根据s[i] - s[j] == (nums[j+1]+...+nums[i])的性质,
用字典dic记录遍历数组的过程中和为s的个数,那么以第i个元素结尾的和为k的连续子序列个数为:dic.get(s[i]-k, 0).
需要注意的是,初始时和为0,dic应该初始化为dic = {0: 1}.
或者令s[i]表示nums[0]到nums[i-1]的累计和:code2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容