以下题目都来自Leetcode,题解和评论中的一些优秀代码,我改成了python。如有侵权,请联系博主。
5-16-面试题43. 1~n整数中1出现的次数
考点:找规律和面试题44. 数字序列中某一位的数字有点像
5-17-面试题60. n个骰子的点数
考点:二维DP题
二维DP题中的状态方程,是和上一行所在的状态和此时刻所在的状态相关的。
dp[n][s] 表示共有n个骰子 当其总和为s时,具有的方案数。
dp[i][s] = dp[i][s] + dp[i-1][s-j] 当前i个骰子总和 = 当前i个骰子总和 + 上一个筛子(i-1)中可以正好凑到s-j的方案数。j当前筛子可以的数字。
一共有三个骰子, dp[3][7] = dp[2][2] + dp[2][3] + dp[2][4]+dp[2][5] +dp[2][6] 表示的是 s-j 得在i-1个筛子可以的总和数内。
当前两个骰子是2时,最后一个骰子可以是5。 最后一个骰子只有一种方案。
当前两个骰子是3时,最后一个骰子可以是4。 最后一个骰子只有一种方案。
当前两个骰子是4时,最后一个骰子可以是3。 最后一个骰子只有一种方案。
当前两个骰子是5时,最后一个骰子可以是2。 最后一个骰子只有一种方案。.....
题解来自此处。
class Solution:
def twoSum(self, n: int) -> List[float]:
L = [[ 0 for _ in range(0,6*n+1)] for _ in range(0,n+1)] # 建立 (n+1)*(6n+1)
for i in range(1,7):
L[1][i] = 1 # n=1 时候6个骰子方案都是1
for i in range(2,n+1): # 控制几个骰子
for j in range(i,6*i+1): #控制总和
for k in range(1,7): #控制每个骰子的点数
if j-k >= 1: # 保证前i-1个骰子可以出来这个总和且最起码要剩一个点数给最后一个骰子
L[i][j] = L[i][j] + L[i-1][j-k] # j-k 前i-1个骰子的总和
ans = [] # 最后记录
for i in range(n,6*n+1):
ans.append((L[n][i]*1.0)/pow(6,n))
return ans
5-18-面试题65. 不用加减乘除做加法
考点:位运算 python 负数补码的问题
将a,b上的从右到左每一位 异或操作 (0+0) =0 (1+0) = 1 (0+1) = 1 (1+1) = (1)0 => n
当a,b两位均是1时[&],要向前进一位(<<)。 => c 这样将n+c也可以得到正确的加法。但是题目不让用加法,因此再将n和c当做a,b继续做加法,直至c进位为0。即把累加结果全部累加到了n中。
注意:在计算机系统中,数值一律用 补码 来表示和存储。补码的优势: 加法、减法可以统一处理(CPU只有加法器)。因此,以上方法 同时适用于正数和负数的加法 。
class Solution:
def add(self, a: int, b: int) -> int:
x = 0xffffffff
a, b = a & x, b & x
while b != 0:
a, b = (a ^ b), (a & b) << 1 & x
return a if a <= 0x7fffffff else ~(a ^ x)
这个题涉及到了python补码和原码的形式,所以为了搞清楚,从开了一篇博客。
5-19-面试题67. 把字符串转换成整数
考点:逻辑?我觉得逻辑清楚的,应该能够比较容易的写出。首先判断最前面的空格,然后判断+,-,然后再算数。
自己的代码: O(N) O(N)
class Solution:
def strToInt(self, string: str) -> int:
flag = 0
for i in range(0,len(string)):
if string[i] == ' ':
flag += 1
else:
break #找出所有前空格
string = string[flag:]
def t(st,ans):
if st == '1':
ans.append(1)
if st == '2':
ans.append(2)
if st == '3':
ans.append(3)
if st == '4':
ans.append(4)
if st == '5':
ans.append(5)
if st == '6':
ans.append(6)
if st == '7':
ans.append(7)
if st == '8':
ans.append(8)
if st == '9':
ans.append(9)
if st == '0':
ans.append(0)
if st not in ['0','1','2','3','4','5','6','7','8','9']:
return ans
return ans
def change(ans,flag):
x = 1
an = 0
for j in ans[::-1]:
an += j*x
x *= 10
if flag == '1' or flag == '+':
return an
else: return an*(-1)
if len(string) == 0 : return 0
ans = []
if string[0] in ['+','-']:
i = 0
for st in string[1:]:
i += 1
ans = t(st,ans)
if len(ans) < i:
break
ans = change(ans,string[0])
if ans > 2**31-1:return 2**31-1
elif ans < -2**31 :return -2**31
return ans
i = 0
ans = []
for st in string:
i += 1
ans = t(st,ans)
if len(ans) <i:
break
#print(ans)
ans = change(ans,'1')
if ans > 2**31-1:return 2**31-1
return ans
不足点:(1)字符串转int型变量时,可以利用 str(i) - str('0')来实现转换。相当于用 i的ASCII码值减'0'的ASCII。
(2)算数的时候,不用非要从后往前算,也可以从前往后算。
a = [1,3,4,6] #从前往后
ans = 0
for i in a:
ans = ans*10 + i
a = [1,3,4,6] #从后往前
ans,j = 0,1
for i in a[::-1]:
ans += j*i
j *= 10
(3)由于python是可以不考虑溢出的,所以可以放在最后进行是否在32位有效范围内,但是如果在计算数字过程中[每一次都进行一次判断],那么会节省一部分的时间。
根据题解,溢出有两种。(一),累计之前res>bndry ,直接跳出。因为在往下走的时候,是res*10 > 21474836500
因为bndry 是 214748364 那么大于它的最小的是 214748365, 乘以10之后,肯定溢出。
(二)res=bndry,因为下一步要做 res*10+i,如果i>7那么也是溢出的情况。因为复数是取到'9'时,溢出。正数是'8'溢出。
综合上述三点不足,代码如下。[来自题解]
class Solution:
def strToInt(self, string: str) -> int:
if len(string) ==0:return 0
i,flag,ans = 0,0,0
while( i < len(string) and string[i]== ' ' ): i+=1
if i == len(string) : return 0
if string[i] == '-':flag=1
if string[i] in ['-','+']: i+=1
min_ = 2**31
max_ = 2**31-1
bndry= (2**31-1)//10
for c in string[i::]:
if c >'9' or c <'0' : break
if ans>bndry or (ans ==bndry and c>'7'):return min_*(-1) if flag else max_
ans = ans*10 + (ord(c)-ord('0'))
return ans*(-1) if flag else ans
考点:二分查找及其变形 O(logN) 排序数组中的搜索问题 二分边界处理问题。
双指针 O(N)
class Solution:
def search(self, nums: List[int], target: int) -> int:
# dic O(N) O(N)
# 二分查找 O(logN) O(1)
i,j = 0,len(nums)-1
while(i<j and nums[i]<target):i+=1
while(i<j and nums[j]>target):j-=1
if i==j:
if nums[i] == target:
return 1
else:
return 0
return j-i+1
此题二分法不在于找元素,而在于找边界。在普通二分法中,找的是nums[m] = target的 m。
class Solution:
def search(self, nums: List[int], target: int) -> int:
# 二分查找 O(logN) O(1)
# 两次二分,找出最左边界和最右边界
def left(l,r): #找左边界
while(l<=r):
mid = l+(r-l)//2
if nums[mid] > target:
r = mid-1
elif nums[mid]<target:
l = mid+1
elif nums[mid] == target:
r = mid-1 #在左边继续找i
if l >= len(nums) or target != nums[l] :return -1 #未找到左边界,即值不在里面
return l #if target == nums[l] return l else l
def right(l,r):
while(l<=r):
mid = l+(r-l)//2
if nums[mid] > target:
r = mid-1
if nums[mid] < target:
l = mid+1
elif nums[mid] == target:
l = mid+1
if r == -1 or target != nums[r] :return -1 #左边界不存在
return r
if len(nums) == 0:return 0
i = left(0,len(nums)-1)
if i ==-1 :return 0 # 不存在左边界
j = right(0,len(nums)-1)
return j-i+1
5-20-面试题06. 从尾到头打印链表
考点:链表
class Solution:
def reversePrint(self, head: ListNode) -> List[int]:
l = []
while head:
l.append(head.val)
head = head.next
return l[::-1]
# 原地翻转 O(1)
i,j = 0,len(l)-1
while(i<j):
temp = l[i]
l[i] = l[j]
i += 1
l[j] = temp
j -= 1
return l
5-21-面试题24. 反转链表
考点:链表的基本使用
(1)先按照顺序存储到栈[先进后出],再重新构造
(2)直接在原链表上操作 pre=None cur=head 将cur指向pre指针,构造第一个指针指向None。
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
pre = None
cur = head
next_ = None
while(cur):
next_ = cur.next
cur.next = pre
pre = cur
cur = next_
return pre
5-22-面试题18. 删除链表的节点
考点链表的基本操作,这个题和剑指offer上不太一样。先以链接的为主。
我写的:(1)最简单的就是用一个栈来存节点,然后再拼接。时间复杂度O(2N) 空间复杂度O(2N)
(2)不用栈存,直接在原链表上做。O(N)空间复杂度O(N)
class Solution:
def deleteNode(self, head: ListNode, val: int) -> ListNode:
if head.val == val:return head.next #直接 在原链表上做
H = ListNode(0)
H_1 = H
while (head):
if head.val != val:
H_1.next = head
H_1 = H_1.next
else:
H_1.next = head.next #跳过该节点,否则head后面的全在H_1后面
head = head.next
return H.next
''' 题解中我认为比我写得好的样子
if head == None: return None
if head.val == val:return head.next
P = head
while(P.next.val != val): P = P.next
P.next = P.next.next
return head '''
5-23-面试题52. 两个链表的第一个公共节点
考点:链表的基本常识
(1)相交指的是地址相同而不是值相同。 headA == headB not headA.val == headB.val
(2)想的是将链表反转后,比较从头开始。那么遇到不同的时候,那么就结束了。
但是这样比较的思想是不对的,因为[2,3,4,5],[2,3,1,0] headA != headB 因为他们节点是不同的。只能说
headA.val = headB.val。
(3)题目要求不改变原结构,利用head = head.next 会更改原结构,只剩下最后一个节点。所以利用node=headA来做。?这种情况为什么不会改变原链表结构,这个按照道理来说应该也是浅拷贝问题。
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
dic = {} ##用字典去直接存A的所有节点,然后再继续做 O(N) O(N)
node1 = headA
node2 = headB
while(node1):
dic[node1] = 1
node1 = node1.next
while(node2):
if dic.get(node2,0) : return node2
node2 = node2.next
return None
上面的思路来自题解1,下面这种思路来自题解2。 两个链表长度分别为L1+C、L2+C, C为公共部分的长度,做法: 第一个人走了L1+C步后,回到第二个人起点走L2步;第2个人走了L2+C步后,回到第一个人起点走L1步。 当两个人走的步数都为L1+L2+C时就两个就相遇了。摘自:题解二评论 @eatalot
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
# A:m+n+k B:n+m+k 如果有相同的节点,以这样的方式走,那么肯定会有重合
node1,node2 = headA,headB
while(node1 != node2):
node1 = node1.next if node1 else headB
node2 = node2.next if node2 else headA
return node1
5-24-面试题35. 复杂链表的复制
考点:深拷贝和浅拷贝[引用] 这个题目中的复制,意在实现深拷贝,即开辟另外一块内存空间来存储复制样本。
注意点:(1) head 要每次重建,Node(head.val) 但是 head.random 不能重建,因为他要指向 已建立的节点的内存地址。所以都重新创建,然后再分别指向这种想法是错误的。
那么要做的就是重建head,然后head.random如果存在直接指向,如果不存在在建立。
5-25-面试题30. 包含min函数的栈
考点:如何将min()缩小到O(1),因为排序算法时间复杂度最低为O(NlogN)。所以得想到用空间换时间。
所以 用S2去维护一个最小栈,当pop()时,只要不涉及到最小值弹出,那么栈的最小值,不会改变。当push()时,只要push()进来元素没有最小值大,那么就不用更新最小值,因为无论增加多少,最小值依旧不变。这也是单调栈的思路。
class MinStack:
def __init__(self):
"""
initialize your data structure here.
"""
self.S1 = []
self.S2 = []
def push(self, x: int) -> None:
self.S1.append(x)
if len(self.S2) == 0 or x <= self.S2[-1]: self.S2.append(x)
def pop(self) -> None:
if len(self.S1) != 0: a = self.S1.pop(-1)
else: return None
if a == self.S2[-1]:
self.S2.pop(-1)
def top(self) -> int:
if len(self.S1) != 0 :return self.S1[-1]
else:return None
def min(self) -> int:
if len(self.S2) != 0 : return self.S2[-1]
else:return None
5-26-面试题27. 二叉树的镜像
考点:二叉树的知识,主要是如何保留交换前的左右子树。
class Solution:
def mirrorTree(self, root: TreeNode) -> TreeNode:
if not root:return None
q = [root]
ans = root
while(q):
node = q.pop(0)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right) ##先把交换前的状态保存
node.left,node.right = node.right,node.left
return ans
##递归
if not root:return None
root.left,root.right = self.mirrorTree(root.right),self.mirrorTree(root.left)
return root
5-27-面试题58 - II. 左旋转字符串
考点:字符串操作 我认为考点在于如何原地操作->如何利用索引进行原地操作。
(1)字符串切片 O(N) O(N) list和dic的切片复杂度查看此处。
class Solution:
def reverseLeftWords(self, s: str, n: int) -> str:
return s[n:]+s[0:n]
## 第二种比较妙的
##ss = s+s
##return ss[n:n+len(s)]
(2)列表遍历拼接 在不能用切片的情况下。
class Solution:
def reverseLeftWords(self, s: str, n: int) -> str:
L = '' #字符串 O(N) O(N)
for i in range(n:len(s)):
L+=s[i]
for i in range(0,n):
L+=s[i]
return L
L = [] ##列表拼接
for i in range(n,len(s)):
L.append(s[i])
for i in range(0,n):
L.append(s[i])
return ''.join(L)
res = "" ##O(N) O(N)
for i in range(n, n + len(s)): #### 求余 来自 Leetcode @Krahets
res += s[i % len(s)]
return res
利用求余简化for循环,题解来自Leetcode @Krahets。
由于python中字符串是不可变对象,即不能实现s[i] = s[j],因此python代码应该是没办法进行原地操作的[不确定]。
所以基于c有原地操作。[不知道如何写函数了,忘光了,暂且先这样吧]
char* reverseLeftWords(char* s, int n){
int len = strlen(s);
int i = 0;
int j = n-1;
char temp;
while(i<j){ // 一次交换
temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
}
i= n;
j = len-1;
while(i<j){ // 二次交换
temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
}
i = 0;
j = len-1;
while(i<j){ // 三次交换
temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
}
return s;
}
5-28-面试题28. 对称的二叉树
考点:感觉二叉树的题目,多半能用递归来做,不能用递归的一般都是层次遍历的变形题。
这个评论中 @Tai_Park写个很好,整理了一下递归思路。
递归三件事:
(1)题目想要得到什么 ->判断是否为镜像 那么return 的就应该是 true/fasle 判断的树的左右两棵树,那么传入最起码是两个参数left right
(2)递归终止条件 即【return】的是什么
(3)层次传递之间的关系是什么
做递归思考三步:
- 递归的函数要干什么?
- 函数的作用是判断传入的两个树是否镜像。
- 输入:TreeNode left, TreeNode right
- 输出:是:true,不是:false
- 递归停止的条件是什么?
- 左节点和右节点都为空 -> 倒底了都长得一样 ->true
- 左节点为空的时候右节点不为空,或反之 -> 长得不一样-> false
- 左右节点值不相等 -> 长得不一样 -> false
- 从某层到下一层的关系是什么?
- 要想两棵树镜像,那么一棵树左边的左边要和二棵树右边的右边镜像,一棵树左边的右边要和二棵树右边的左边镜像
- 调用递归函数传入左左和右右
- 调用递归函数传入左右和右左
- 只有左左和右右镜像且左右和右左镜像的时候,我们才能说这两棵树是镜像的
- 调用递归函数,我们想知道它的左右孩子是否镜像,传入的值是root的左孩子和右孩子。这之前记得判个root==null。
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
def t(L,R): ## O(N) O(N)
if not L and not R:return True
if not L or not R or L.val != R.val:return False
return t(L.left,R.right) and t(L.right,R.left)
if not root :return True
else:
return t(root.left, root.right)
除了递归之外,还有其他方法:利用中序遍历每个节点保存成一个list,然后判断这个list是否是回文结构。
BFS 队列实现,层次遍历。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root : return []
L = []
q = [root]
while(q): #O(N) O(3N)
size = len(q)
ans = []
for i in range(0,size):
cur = q.pop(0) ## 用pop()来选择pop()出几个
ans.append(cur.val)
if cur.left :
q.append(cur.left)
if cur.right:
q.append(cur.right)
L.append(ans) ##O(N)
return L
DFS实现
DFS说白了就是递归实现,那么思考几个问题:
(1)最后需要的是什么?每一层的节点信息。如何实现每一层?得用一个depth统计每一层。
(2)什么时候返回,停止递归?node是空时。返回什么?不用返回什么,因为用其他去存储想要的东西了
(3)递归传递关系是什么?传递node的左右子树和对应的深度信息(+1)。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
## DFS
##
dic = {} ## depth: []
## 如何向每一层添加 ?
def t(node,depth): # 递归是想要获取每一层的节点,那么就要设置一个depth
if not node :return ##递归返回条件
if depth in dic.keys():
dic[depth].append(node.val) # 每一层存入相同的深度中,用了字典
else:
dic[depth] = [node.val]
if node.left:
t(node.left,depth+1) ##向下传递
if node.right:
t(node.right,depth+1)
t(root,0)
return [[val for val in dic[depth] ] for depth in dic.keys()]
看了评论之后,发现还是有地方可以更改的:
(1)存储结构,可以不用dic,直接用list
(2)不用判断 node.left是否存在,因为如果不存在就直接return了,那么省掉了大量的判断过程。
来自此题评论中的@MoffySto。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
L = []
## 如何向每一层添加 ?
def t(node,depth): # 递归是想要获取每一层的节点,那么就要设置一个depth
if node: ##省略每一层 node==None 判断
#if depth in dic.keys():
if depth >= len(L): # L = [[1],[2,3]]
#dic[depth].append(node.val) # 每一层存入相同的深度中
L.append([]) #再多创建一层
L[depth].append(node.val) # 深度和L中的index对应的
t(node.left,depth+1)
t(node.right,depth+1)
t(root,0)
return L
5-30-面试题32 - I. 从上到下打印二叉树
考点:简单的层次遍历
看了题解,发现一个知识点:q.pop(0)的时间复杂度是O(N),因为 list 是列表,也就是 java c++ 中的数组移除头部元素的方式 是把后面的元素全部往前移动一位,所以复杂度是 O(N) ; deque 是双端队列,底层是链表,因此头部和尾部是等价的,插入删除都是 O(1)。
因此可以用双端队列,代替list结构,来实现层次遍历。
if not root:return []
ans = []
q = collections.deque() #双端队列
q.append(root)
while(q):
node = q.popleft()
ans.append(node.val)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
return ans
5-31-面试题32 - III. 从上到下打印二叉树 III
考点:二叉树的层次遍历的变种
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root:return []
q,depth = [root],0
ans = []
while(q):
size = len(q)
s = []
for i in range(0,size):
node = q.pop(0)
if depth%2 == 0:## 冗余判断
s.append(node.val)
else:
s.insert(0,node.val)
if node.left :
q.append(node.left)
if node.right:
q.append(node.right)
ans.append(s)
depth+=1
return ans
分析一下:(1)list.insert()时间复杂度是O(N) (2)每个节点都要判断一次 depth的奇偶性 (3)用ans长度来判断奇偶性,减少变量的使用
改进方法1:用双端队列代替list,实现左插入和右插入,从而实现O(1)的复杂度。
if not root:return []
q,ans = collections.deque(),[] ##改成双端队列
q.append(root)
while(q):
size = len(q)
s = collections.deque()
for i in range(0,size):
node = q.popleft() ##O(1)
if len(ans)%2 == 0:## 右插入
s.append(node.val) #O(1)
else:
s.appendleft(node.val) #左插入 #O(1)
if node.left :
q.append(node.left) #O(1)
if node.right:
q.append(node.right)
ans.append(list(s)) # 时间复杂度 O(N)
return ans
改进方法2:由于每一个节点都要进行一次判断,可以写两遍for来实现奇偶层,从而节省每次判断。
if not root:return []
q,ans = collections.deque(),[]
q.append(root)
while(q):
s = []
for i in range(0,len(q)): #偶数层
node = q.popleft()
s.append(node.val) #左读
if node.left :
q.append(node.left) # 又插入
if node.right:
q.append(node.right)
ans.append(list(s))
if not q :break
s = [] #奇数层
for i in range(0,len(q)):
node = q.pop() # 右读
s.append(node.val)
if node.right:
q.appendleft(node.right) # 左插入
if node.left :
q.appendleft(node.left)
ans.append(s)
return ans
方法3:先输出正常的每层节点,在判断的的时候,在奇数层添加list[::-1],倒序结构就行了。
if not root:return []
q,ans = collections.deque(),[]
q.append(root)
while(q):
s = []
for _ in range(len(q)):
node = q.popleft()
s.append(node.val)
if node.left: q.append(node.left)
if node.right: q.append(node.right)
ans.append( s[::-1] if len(ans)%2 ==1 else s) ##这里使用
return ans
以上三种额外的解法来自此处。