剑指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 证明:
类似于上题,
若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、 [数组中数字出现的次数 II](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()。
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指向了初始链表的尾节点。
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(),超时。
方法二:code2
动态规划。定义dp数组,dp[i][j]表示i个筛子实现点数为j的可能个数。
初始化:dp[0][1]~dp[0][6]=1。
状态转移方程:

时间复杂度:O(),空间复杂度O()
优化空间复杂度:
使用一维dp数组。注意边界条件。
31、栈的压入、弹出序列 code
利用一个栈stack模拟。idx初始指向出栈序列第一个元素。遍历入栈序列,当某元素不等于出栈序列idx所指元素时,将该元素入栈。否则不断出栈,同时idx加一,直至栈顶元素不等于idx所指元素。
16、数值的整数次方
方法一:code1
不断累乘,时间复杂度O(n),超时。
方法二:code2
当n为偶数时,

当n为奇数时,

根据上述公式,可不断将x平方,同时n整除2。用变量res累乘n为奇数时多出的x。最终n等于1时x会被乘到res中,res即最终解。
时间复杂度:O()。
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,小数点是否出现过。该题目较 复杂!!!

你可能感兴趣的:(剑指Offer刷题)