文章目录
- 剑指offer38:字符串的全排列
- 剑指offer39:数组中出现次数超过一半的数
- 剑指offer40:最小的K个数
- 剑指offer41:数据流中的中位数
- 剑指offer42:连续子数组的最大和
- 剑指offer43:1~n整数的十进制中1出现的次数
- 剑指offer44:数字序列中某一位的数字
- 剑指offer45:把数组排成最小的数
- 剑指offer46:把数字翻译成字符串
- 剑指offer47:礼物的最大价值
- 剑指offer48:最长不含重复字符的子字符串
- 剑指offer49:丑数
- 剑指offer50:第一个只出现一次的字符
- 剑指offer51:数组中的逆序对
- 剑指offer52:两个链表的第一个公共节点
- 剑指offer53-1:在排序数组中查找数字
- 剑指offer53-2:0~n-1中缺失的数字
- 剑指offer54:二叉搜索树的第K大节点
- 剑指offer55-1:二叉树的深度
- 剑指offer55-2:平衡二叉树
- 剑指offer56-1:数组中数字出现的次数
- 剑指offer57-1:和为s的两个数字
- 剑指offer57-2:和为s的连续正数序列
剑指offer38:字符串的全排列
以abc为例: 先确定第一位,固定住第一位;可选的有a\b\c 假如第一位是a:再确定第二位,固定住第二位,可选的有b\c 假如第二位是b:再确定第三位,可选的只有c
更换选择的方法是交换位置,假如第一次排列进行选择时,固定的第一位是选了a,下一次排列的时候,将a后面的元素b与a交换,这样就变成了第一位固定选了b,剩下的从a\c中选;同理,第二位的选择更改也是跟第二位后面的元素交换。
考虑到重复元素的情况,可以设置一个set,每固定住一个元素就计入set中,每次选择的时候先对比set中是否已经有相同的元素,再进行固定。
class Solution:
def permutation(self, s: str) -> List[str]:
# 字符串不支持交换操作,更改选择需要用到交换,所以将字符串转成列表
s_list = list(s)
# 初始化返回结果集
res =[]
# 确定第n位的字符
def order(n):
# 递归终止条件
if n == len(s)-1:
res.append(''.join(s_list)) #前面把字符串转换成了列表
# 返回的应该是一个字符串,所以转换成字符串
# set记录前面已选的元素,防止重复
set = []
# 选择第n位固定谁,从n往后进行交换
for i in range(n,len(s)):
if s_list[i] in set: # 如果是重复元素,跳出这一步
continue
set.append(s_list[i]) # 如果不是重复元素,记录到set中
# 每确定一位元素,就进行交换,从而递归调用下一次排序
s_list[n],s_list[i] = s_list[i],s_list[n]
# 递归下一位
order(n+1)
# 交换回去,否则会混乱
s_list[i],s_list[n] = s_list[n],s_list[i]
# 主函数中调用递归函数
order(0)
return res
剑指offer39:数组中出现次数超过一半的数
题目已经说了,出现次数超过一半,说明一半的数是同一个数,最简单的方法就是排序后的数组中点就是这个数
class Solution:
def majorityElement(self, nums: List[int]) -> int:
nums.sort()
return nums[len(nums)//2]
剑指offer40:最小的K个数
排序找前k个数
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
arr.sort()
res =[]
for i in range(k):
res.append(arr[i])
return res
剑指offer41:数据流中的中位数
以后再撕
剑指offer42:连续子数组的最大和
一开始的想法是排个序然后倒序求和,后来意识到python不区分负数,所以排序的方法就行不通。
那就老老实实的从数组开始,每遇到一个数就求和。记当前第一个元素curnum =nums[i],curnum表示当前最大值;并且设sum记录当前连续子数列的和,初始为0.
然后遍历整个数组进行比较,如果当前的和与数组的第 i 个元素相加小于第 i 个元素,说明前面已加的和不如直接从第 i 个元素开始加得到的数大,那么不如就丢弃前面的和,直接从第 i 个元素开始重新往后加。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 记录数组中最大值,初始是是第一个元素
cur_num=nums[0]
# 记录最大和,初始为0
res= 0
# 比较整个数组
for i in range(len(nums)):
# 如果当前元素加上前面的和,还不如这个元素本身大,就从这个元素开始找连续数组
if res+nums[i]<nums[i]:
res=nums[i]
# 否则就加进和中找下一个
else:
res+=nums[i]
# 更新cur_num,取最大的那个
if cur_num<res:
cur_num=res
return cur_num
剑指offer43:1~n整数的十进制中1出现的次数
没看懂这个题解
剑指offer44:数字序列中某一位的数字
需要明确一点,当数字只有0-9的时候,一个数字就占一位下标,但是超过个位的数字序列化以后就占好几个小标,比如10,11,12,13序列化后10占下标11和12。也就是个位,十位,百为,千位……都占一个索引下标。参考 来找一下规律吧: 1位数:范围1-9,起始点1,数量9,占序列化后长度9 2位数:范围10-99,起始点10,数量90,长度180(90个数每个序列化后占两位) 3位数,范围100-999,起始点100,数量900,长度2700 …… 可以看到,起始点从1开始,每加一个位数,起始点要乘以10 每个位数范围内的数量满足关系: 9×起始点 每个位数范围内的长度满足关系; 数量×位数
所以我们需要设置一个digit表示位数,初始是1;迭代公式digit+1 start表示不同位范围内数字的起始点,初始是1;迭代公式9×start length表示序列化后占据的长度,初始是9;迭代公式9×start×digit 根据上面我们得到的规律 长度length= 9×起始点start×位数digit
要找序列化后的一个下标n位对应的是哪个数字,就要先找到它在哪个位数的长度内,即确定位数。然后找到这个位数的数字的起始点start,算n在哪个数中,然后再算n是这个数的第几位
class Solution:
def findNthDigit(self, n: int) -> int:
digit = 1
start = 1
length = 9
# 循环执行 n减去 一位数、两位数、... 的数位数量所占长度
while n>length:
n-=length
start*=10
digit+=1
length=9*start*digit
# 此时的n是从digit位数的起始数字start开始计数
# 确定n所在的数字
num = start + (n - 1) // digit
# 确定n在该数字的哪一位
s = str(num) # 转化为 string
res = int(s[(n - 1) % digit]) # 获得 num 的 第 (n - 1) % digit 个数位,并转化为 int
return res
剑指offer45:把数组排成最小的数
一个数组里可以有多个数[3,30,34,5,9],拼接后可以有多个数[3303459][3034593]……最小的那个是[3033459]。
一开始的想法是有点基数排序的思想,先按个数为排序,然后十位数排序,排完后拼的那个数就是最小的拼接数。但是后来意识到3和321排完个位后没法比较,遂放弃。
如果单个值排序是没有意义的,应该看组合起来的排序结果,而且两个数一组合,不同的组合方式位数也是一样的,[3][30]有两种组合方式[303][330],看排序后谁大谁小。这里就需要用到转字符串str()方法,还有排序,选择quicksort()
class Solution:
def minNumber(self, nums: List[int]) -> str:
def quick_sort(l,r):
if l>=r:
return
i = l
j = r
while i<j:
while s[i]+s[l]<=s[l]+s[i]:
i+=1
while s[j]+s[r]>=s[r]+s[j]:
j-=1
s[i],s[j]=s[j],s[i]
s[i],s[l]=s[l],s[i]
quick_sort(l,i-1)
quick_sort(i+1,r)
s = [str(num) for num in nums]
quick_sort(0,len(s)-1)
return ''.join(s)
剑指offer46:把数字翻译成字符串
每走一步有两种选择,翻译自己,还是跟别人组合一起翻译,这种一步有多种选择的方法很自然就想到要用动态规划去做。那我们就要先找一下动态规划的转移方程和边界条件。
dp(1)表示一个字符的时候的翻译方式,显然,只有一种翻译方式; dp(2)表示有两个字符时的翻译方式是,显然有两种翻译方式,要么是组合起来翻译,要么是单独翻译; …… dp(n)表示有n个字符时的翻译方式。
假如我们现在翻译到了第n个数,怎么翻译到这个数的呢?要么翻译他自己dp(n-1),要么翻译它跟前面一个数的组合dp(n-2),而且要求组合的数介于0-25之间。如果组合数大于区间,就只能翻译自己,如果组合数介于区间内,就可以翻译组合数。所以转移方程的限制条件就出来了:组合数能不能再区间内,能就dp(i-1)+dp(i-2),不能就dp(i-1).
找到一个讲的比我好的看下面这张图:
写代码的时候需要注意,输入的num是一个数而不是一个数组,如果这个数只有一位(也就是0-9),就没必要组合了;如果是多位数,就要先将其转换成字符串s,这样才能按位取数
class Solution:
def translateNum(self, num: int) -> int:
# 如果是个位数,直接返回1,只有1中翻译方法
if num<10:
return 1
# 如果是多位数,转字符串
s = str(num)
n = len(s)
# 初始化dp,从dp[0]到[n]都初始为1,为了取到闭区间所以要n+1
# 至于dp[0]为什么也取1,是因为dp[2]=[1]+[0],如果不取1,dp[2]得不到2
dp = [1 for _ in range(n+1)]
# 从dp[2]开始实现转移方程,注意也要取的闭区间
for i in range(2,n+1):
# 判断组合数0-25就是判断前前一个数是1还是2,1就可以组合,2就需要判断前一个数是否在6之前
# 能组合翻译
if s[i-2]=='1' or (s[i-2]=='2' and s[i-1]<='5'):
dp[i]=dp[i-2]+dp[i-1]
# 不能组合翻译
else:
dp[i]=dp[i-1]
return dp[n]
剑指offer47:礼物的最大价值
也涉及到一步可以有多个选择的问题,所以仍然用动态规划取解决。先找转移方程:现在在一个grid(i,j),要么是左边走过来的,要么是右边走过来的,显然哪个大走哪个过来的,得到
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
class Solution:
def maxValue(self, grid: List[List[int]]) -> int:
row = len(grid)
col = len(grid[0])
for i in range(row):
for j in range(col):
if i == 0 and j == 0: continue
if i == 0: grid[i][j] += grid[i][j - 1]
elif j == 0: grid[i][j] += grid[i - 1][j]
else: grid[i][j] += max(grid[i][j - 1], grid[i - 1][j])
return grid[-1][-1]
剑指offer48:最长不含重复字符的子字符串
第一个想法就是双指针,一个i不动,另一个j往后找,遇到不等于i的就继续往后找,但是这个想法有一个致命的缺点,就是只能保证不重复i,i-j之间会有其他的元素重复。
所以考虑滑动窗口,保障从left到right窗口中的元素始终不同。滑动窗口的原理是同一时刻一个动,另一个不动,所以让tail动,如果元素不在窗口内就持续移动,比窗口哪个长,保留长的哪个结果值。如果元素在窗口内就动left直到重复的元素,把前面的重复元素删除。对
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# 滑动窗口左指针
left =0
# 滑动窗口右指针
right = 0
# length记录长度,初始为1,因为即使是只有一个元素子元素长度也是1
length = 1
# 特殊情况,当少于或只有一个元素的时候不需要窗口,直接返回长度
if len(s)<=1:
return len(s)
# 滑动窗口滑动的条件为数组结束
while right<len(s)-1:
right+=1
# 如果right指的哪个元素不在窗口内,加入并记录长度,保留大的结果
if s[right] not in s[left:right]:
length = max(right - left + 1,length)
# 如果right的元素已经在窗口中有了,就从前面删除窗口重复的元素
else:
while s[right] in s[left:right]:
left+=1
return length
剑指offer49:丑数
暂时不撕
剑指offer50:第一个只出现一次的字符
第一个想法就是字典,遍历字符串,然后遇到一个就放进字典中,如果字典中已经有这个键,值就加1,如果没有就在字典中增加键值对;此外,为了统计第一次出现的,设一个order记录加进dict的顺序,遍历order时遇到的第一个dict为1的就是第一次出现的字符
class Solution:
def firstUniqChar(self, s: str) -> str:
dict={}
order =[]
for ss in s:
if ss in dict:
dict[ss]+=1
else:
dict[ss]=1
order.append(ss)
for i in order:
if dict[i]==1:
return i
return ' '
剑指offer51:数组中的逆序对
以后再撕
剑指offer52:两个链表的第一个公共节点
定义指向两个链表的指针p和q,p不动,q找链表2中与p相同的,找到了就是第一个公共节点,找不到就遍历完一次q后,p指向下一个,再遍历一次q (but超时额) 两个有公共节点的链表,那么从公共节点开始一直到链表结束,这部分长度也是公共的,假设是m。那么两个链表的长度分别是L1+m,L2+m。先求出两个链表的长度,然后让长的链表先走,走至两个链表的剩余部分一样长,然后开始找公共元素。(图解)
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
# 定义指向头节点的指针
p = headA
q = headB
# 初始化长度变量
lengthA = 0
lengthB = 0
# 计算A的长度
while p is not None:
p = p.next
lengthA+=1
# 计算B的长度
while q is not None:
q = q.next
lengthB+=1
# 指针归位
p=headA
q=headB
# 找除长链表,先走差值步
if lengthA>=lengthB:
for i in range(lengthA-lengthB):
p = p.next
else:
for i in range(lengthB-lengthA):
q =q.next
# 从公共长度部分开始找相同元素
while p!=q:
p = p.next
q = q.next
return p
剑指offer53-1:在排序数组中查找数字
定义一个字典,遍历数组,每遇到一个数,就加进字典,如果字典中已经有了该键就值+1,用字典统计元素出现的次数。最后在字典中查找键,返回该键的值,如果找不到就返回0
class Solution:
def search(self, nums: List[int], target: int) -> int:
dict ={}
for i in range(len(nums)):
if nums[i] in dict:
dict[nums[i]]+=1
else:
dict[nums[i]]=1
if target in dict:
return dict.get(target)
else:
return 0
剑指offer53-2:0~n-1中缺失的数字
首先数列是递增的,其次缺失的数字只有一个,考虑二分查找法.
如果缺失的数字在后半部分,那么前半部分应该是正好第中间位的值等于第中间个下标,如果缺失的数字在前面,那么前半部分肯定不是0~mid刚好排序,中间位不能对齐。所以只需要判断中间位的值是不是等于中间位的下标就可以了。是则找后半部分,否则找前半部分。
class Solution:
def missingNumber(self, nums: List[int]) -> int:
left = 0
right = len(nums)-1
while left<=right:
mid = (right+left)//2
if nums[mid] == mid:
left = mid+1
else:
right = mid-1
return left
剑指offer54:二叉搜索树的第K大节点
首先见二叉搜索树就想其性质:左子树<根<右子树。按中序遍历的顺序,可以得到一个递增的序列。而中序遍历的倒序是一个递减序列。
class Solution:
def kthLargest(self, root: TreeNode, k: int) -> int:
def inorder(root):
if root is None:
return
inorder(root.right)
self.k -=1
if self.k==0:
self.res = root.val
inorder(root.left)
self.k = k
self.res = None
inorder(root)
return self.res
剑指offer55-1:二叉树的深度
考虑树的深度优先遍历方式,其中dfs包括先序、中序、后序,考虑用后序遍历,先求出左右子树的深度,选最大的那个,然后再加1算上根节点就是树的深度了。
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if root is None:
return 0
l = self.maxDepth(root.left)
r = self.maxDepth(root.right)
return max(l,r)+1
剑指offer55-2:平衡二叉树
承接上题,上题求二叉树的深度,这个题需要比较二叉树的深度,所以需要写一个函数求树高,然后比较左右子树的差的绝对值,表示绝对值的函数用abs(),然后再调用函数自身递归看子树是否平衡,也就是需要两个递归。
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
# 计算树高
def depth(node):
if node is None:
return 0
l = depth(node.left)
r = depth(node.right)
return max(l,r)+1
# 回到主函数
if not root:
return True
# 算根节点的左右子树高度
left = depth(root.left)
right = depth(root.right)
# 如果根节点满足平衡
if abs(left-right)<=1:
# 递归,检查子树的子树是否是平衡
return self.isBalanced(root.left) and self.isBalanced(root.right)
return False
剑指offer56-1:数组中数字出现的次数
一开始没有仔细审题,想用一个字典记录数组出现次数,后来发现题目要求空间复杂度O(1),果断放弃。
搜了一下题解,需要用到计算知识——异或,一个数字和与他相同的数字异或结果是0,和与他不同的数字异或结果是1
进行一次全员异或操作,得到的结果就是那两个只出现一次的不同的数字的异或结果。
(待续)
剑指offer57-1:和为s的两个数字
使用滑动窗口,看窗口的左边缘和右边缘的和,左边不动,如果左边和右边和小于s,就移动右边,如果大于,就移动左边,直至窗口的左右之和等于s或者找不到。
但是数组递增的性质就没有用上,考虑到这个要求也算一种条件,所以从递增角度,用滑动窗口就可以考虑一开始窗口就是整个数组(头到尾),如果此时的和比要找的大,就把窗口的尾部左移,先减掉大的;如果此时的和比要找的小,就向后移动左指针。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
i = 0
j = len(nums)-1
while i<j:
s = nums[i]+nums[j]
if s>target:
j-=1
elif s<target:
i+=1
else:
return nums[i],nums[j]
return []
剑指offer57-2:和为s的连续正数序列
注意审题,要求输出的是一个连续的数列。上一题用了滑动窗口思想,这一题应该也需要用到滑动窗口。其实就是从上一个计算窗口边缘的和变为了计算窗口内所有元素的和。由于也是递增序列,只要和不够,就往右走,走到和够,再往右走就每必要了;所以找到一个之后缩小窗口,也就是左边往右走,如果还大,就再减小窗口;如果不够大,就增大窗口。
大佬的详细说明:
class Solution:
def findContinuousSequence(self, target: int) -> List[List[int]]:
res = []
# 初始窗口的起点和终点
start = 1 # 注意,整个序列都是正整数,所以从1开始计算而不是0
end = 2 # 注意,要求最少含有两个数,所以初始的窗口大小最小是从2开始的
sum = 3 # 计算窗口内所有元素的和
while start<end:
# 找到了,把窗口中的元素列表化放到结果集res中
if sum==target:
res.append(list(range(start,end+1)))
# 小了,往右边走,先走,再加右边的值
if sum<=target:
end+=1
sum+=end
# 大了,往左边走,注意区分,先减去左边缘,再移动左边缘
else:
sum-=start
start+=1
return res