算法&&八股文&&其他
一、算法篇
- python实现单例模式
#参考至https://www.cnblogs.com/mqhpy/p/11296376.html
#1. 使用__new__方法
class Singleton(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
orig = super(Singleton, cls)
cls._instance = orig.__new__(cls, *args, **kwargs)
rerurn cls._instance
class A(Singleton):
pass
# 类A即为单例类
#2.共享属性
# 创建实例时把所有实例的__dict__指向同一个字典,这样它们都具有相同的属性和方法(类的__dict__存储对象属性)
class Singleton(object):
_state = {}
def __new__(cls, *args, **kwargs):
ob = super(Singleton,cls).__new__(cls, *args, **kwargs)
ob.__dict__ = cls._state
return ob
class B(Singleton):
pass
# 类B即为单例类
#3.使用装饰器
def singleton(cls):
instance = {}
def wapper():
if cls not in instance:
instance[cls] = cls(*args, **kwargs)
return instance[cls]
return wapper
@singleton
class C:
pass
# 类C即为单例类
#4.import方法
# 作为Python模块时是天然的单例模式
#创建一个sington.py文件,内容如下:
class Singleton(object):
def foo(self):
pass
mysington = Singleton()
# 运用
from sington import mysington
mysington.foo()
- 输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。
例如输入字符串"I am a student. “,则输出"student. a am I”。
分析:简单(lc151)
双指针模拟
倒序遍历字符串 s ,记录单词左右索引边界 i , j ;
每确定一个单词的边界,则将其添加至单词列表 res ;
最终,将单词列表拼接为字符串,并返回即可。
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
# @param str string字符串
# @return string字符串
#
class Solution:
def ReverseSentence(self , str: str) -> str:
left = right = len(str) - 1
newstr = ''
while left >= 0:
if str[left] == ' ':
newstr += str[left+1:right+1] + ' '
right = left - 1
left -= 1
newstr += str[left+1:right+1]
return newstr
# write code here
- 给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的 最长的回文串 。
在构造过程中,请注意 区分大小写 。比如 “Aa” 不能当做一个回文字符串。
分析:简单(lc409)
贪心:在一个回文串中,只有最多一个字符出现了奇数次,其余的字符都出现偶数次
对于每个字符 ch,假设它出现了 v 次,我们可以使用该字符 v / 2 * 2 次,在回文串的左侧和右侧分别放置 v / 2 个字符 ch,其中 / 为整数除法。如果有任何一个字符 ch 的出现次数 v 为奇数(即 v % 2 == 1),那么可以将这个字符作为回文中心,注意只能最多有一个字符作为回文中心。我们用 ans 存储回文串的长度,由于在遍历字符时,ans 每次会增加 v / 2 * 2,因此 ans 一直为偶数。但在发现了第一个出现次数为奇数的字符后,我们将 ans 增加 1,这样 ans 变为奇数,在后面发现其它出现奇数次的字符时,我们就不再改变 ans 的值
class Solution:
def longestPalindrome(self, s: str) -> int:
ans = 0
count = collections.Counter(s)
for v in count.values():
ans += v // 2 * 2
if ans % 2 == 0 and v % 2 == 1:
ans += 1
return ans
- 给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
分析:middle(lc714)
动态规划:考虑到「不能同时参与多笔交易」,因此每天交易结束后只可能存在手里有一支股票或者没有股票的状态。定义状态 dp[i][0] 表示第 i 天交易完后手里没有股票的最大利润,dp[i][1] 表示第 i 天交易完后手里持有一支股票的最大利润(i 从 0 开始)转移方程如下:
dp[i][0]=max{ dp[i−1][0], dp[i−1][1]+prices[i]−fee}
dp[i][1]=max{ dp[i−1][1], dp[i−1][0]−prices[i]}
对于初始状态,根据状态定义我们可以知道第 0 天交易结束的时候有 dp[0][0]=0 以及dp[0][1]=−prices[0]。因此,我们只要从前往后依次计算状态即可。由于全部交易结束后,持有股票的收益一定低于不持有股票的收益,因此这时候dp[n−1][0] 的收益必然是大于 dp[n−1][1] 的,最后的答案即为dp[n−1][0]贪心:方法一中,我们将手续费放在卖出时进行计算。如果我们换一个角度考虑,将手续费放在买入时进行计算,那么就可以得到一种基于贪心的方法:当我们卖出一支股票时,我们就立即获得了以相同价格并且免除手续费买入一支股票的权利。在遍历完整个数组 prices 之后,我们就得到了最大的总收益
class Solution_dp:
def maxProfit(self, prices: List[int], fee: int) -> int:
n = len(prices)
dp = [[0, -prices[0]]] + [[0, 0] for _ in range(n - 1)]
for i in range(1, n):
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])
return dp[n - 1][0]
#空间优化
class Solution_betterDp:
def maxProfit(self, prices: List[int], fee: int) -> int:
n = len(prices)
sell, buy = 0, -prices[0]
for i in range(1, n):
sell, buy = max(sell, buy + prices[i] - fee), max(buy, sell - prices[i])
return sell
class Solution_greed:
def maxProfit(self, prices: List[int], fee: int) -> int:
n = len(prices)
buy = prices[0] + fee
profit = 0
for i in range(1, n):
if prices[i] + fee < buy:
buy = prices[i] + fee
elif prices[i] > buy:
profit += prices[i] - buy
buy = prices[i]
return profit
- 给你 二维 平面上两个 由直线构成且边与坐标轴平行/垂直 的矩形,请你计算并返回两个矩形覆盖的总面积。
每个矩形由其 左下 顶点和 右上 顶点坐标表示:
第一个矩形由其左下顶点 (ax1, ay1) 和右上顶点 (ax2, ay2) 定义
第二个矩形由其左下顶点 (bx1, by1) 和右上顶点 (bx2, by2) 定义
分析:middle(lc223)
分类讨论区间重叠:两个矩形的水平边投影到 x 轴上的线段分别为 [ax1,ax2]和 [bx1,bx2],竖直边投影到 y 轴上的线段分别为 [ay1,ay2] 和 [by1,by2]。如果两个矩形重叠,则重叠部分的水平边投影到 x 轴上的线段为 [max(ax1,bx1),min(ax2,bx 2)],竖直边投影到 y 轴上的线段为 [max(ay1,by1),min(ay2,by2)],根据重叠部分的水平边投影到 x 轴上的线段长度和竖直边投影到 y 轴上的线段长度即可计算重叠部分的面积。只有当两条线段的长度都大于 0 时,重叠部分的面积才大于 0,否则重叠部分的面积为 0
class Solution:
def computeArea(self, ax1: int, ay1: int, ax2: int, ay2: int, bx1: int, by1: int, bx2: int, by2: int) -> int:
area1 = (ax2 - ax1) * (ay2 - ay1)
area2 = (bx2 - bx1) * (by2 - by1)
overlapWidth = min(ax2, bx2) - max(ax1, bx1)
overlapHeight = min(ay2, by2) - max(ay1, by1)
overlapArea = max(overlapWidth, 0) * max(overlapHeight, 0)#相交部分
return area1 + area2 - overlapArea
- 给定一个整形数组arr,已知其中所有的值都是非负的,将这个数组看作一个柱子高度图,计算按此排列的柱子,下雨之后能接多少雨水。(数组以外的区域高度视为0)
分析:hard(lc42)
动态规划:对于下标 i,下雨后水能到达的最大高度等于下标 i 两边的最大高度的最小值,下标 ii 处能接的雨水量等于下标 i 处的水能到达的最大高度减去height[i]。使用动态规划的方法,可以在 O(n) 的时间内预处理得到每个位置两边的最大高度,创建两个长度为 n 的数组 leftMax 和 rightMax。对于0≤i<n,leftMax[i] 表示下标 i 及其左边的位置中,height 的最大高度,rightMax[i] 表示下标 i 及其右边的位置中,height 的最大高度。显然,leftMax[0]=height[0],rightMax[n−1]=height[n−1]。两个数组的其余元素的计算如下:
当 1≤i≤n−1 时,leftMax[i]=max(leftMax[i−1],height[i]);
当 0≤i≤n−2 时,rightMax[i]=max(rightMax[i+1],height[i])
因此可以正向遍历数组 height 得到数组 leftMax 的每个元素值,反向遍历数组 height 得到数组 rightMax 的每个元素值单调栈:维护一个单调栈,单调栈存储的是下标,满足从栈底到栈顶的下标对应的数组 height 中的元素递减。
从左到右遍历数组,遍历到下标 i 时,如果栈内至少有两个元素,记栈顶元素为 top,top 的下面一个元素是 left,则一定有 height[left]≥height[top]。如果 height[i]>height[top],则得到一个可以接雨水的区域,该区域的宽度是 i−left−1,高度是 min(height[left],height[i])−height[top],根据宽度和高度即可计算得到该区域能接的雨水量。
为了得到left,需要将 top 出栈。在对 top 计算能接的雨水量之后,left 变成新的 top,重复上述操作,直到栈变为空,或者栈顶下标对应的 height 中的元素大于或等于 height[i]。
在对下标 i 处计算能接的雨水量之后,将 i 入栈,继续遍历后面的下标,计算能接的雨水量。遍历结束之后即可得到能接的雨水总量。双指针:注意到下标 i 处能接的雨水量由 leftMax[i] 和 rightMax[i] 中的最小值决定。由于数组 leftMax 是从左往右计算,数组 rightMax 是从右往左计算,因此可以使用双指针和两个变量代替两个数组。
维护两个指针 left 和 right,以及两个变量 leftMax 和 rightMax,初始时 left=0, right=n−1, leftMax=0, rightMax=0。指针 left 只会向右移动,指针 right 只会向左移动,在移动指针的过程中维护两个变量 leftMax 和 rightMax 的值。
当两个指针没有相遇时,进行如下操作:使用height[left] 和 height[right] 的值更新leftMax 和 rightMax 的值;
如果 height[left]<height[right],则必有 leftMax<rightMax,下标 left 处能接的雨水量等于 leftMax−height[left],将下标 left 处能接的雨水量加到能接的雨水总量,然后将 left 加 1(即向右移动一位);
如果 height[left]≥height[right],则必有 leftMax≥rightMax,下标 right 处能接的雨水量等于rightMax−height[right],将下标 right 处能接的雨水量加到能接的雨水总量,然后将 right 减 1(即向左移动一位);
当两个指针相遇时,即可得到能接的雨水总量
class Solution_dp:
def trap(self, height: List[int]) -> int:
if not height:
return 0
n = len(height)
leftMax = [height[0]] + [0] * (n - 1)
for i in range(1, n):
leftMax[i] = max(leftMax[i - 1], height[i])
rightMax = [0] * (n - 1) + [height[n - 1]]
for i in range(n - 2, -1, -1):
rightMax[i] = max(rightMax[i + 1], height[i])
ans = sum(min(leftMax[i], rightMax[i]) - height[i] for i in range(n))
return ans
class Solution_monoStack:
def trap(self, height: List[int]) -> int:
ans = 0
stack = list()
n = len(height)
for i, h in enumerate(height):
while stack and h > height[stack[-1]]:
top = stack.pop()
if not stack:
break
left = stack[-1]
currWidth = i - left - 1
currHeight = min(height[left], height[i]) - height[top]
ans += currWidth * currHeight
stack.append(i)
return ans
class Solution_dptr:
def trap(self, height: List[int]) -> int:
ans = 0
left, right = 0, len(height) - 1
leftMax = rightMax = 0
while left < right:
leftMax = max(leftMax, height[left])
rightMax = max(rightMax, height[right])
if height[left] < height[right]:
ans += leftMax - height[left]
left += 1
else:
ans += rightMax - height[right]
right -= 1
return ans
- 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
分析:hard(lc84)
单调栈: 维护一个单调递增的栈,当栈非空并且栈顶数 >= 遍历到的数时,则弹栈并计算矩形的面积
TRICK:在数组的最后追加一个值为0 的数,作用是弹出栈所有的元素。如图:
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
# @param heights int整型一维数组
# @return int整型
#
class Solution:
def largestRectangleArea(self , heights: List[int]) -> int:
monoStack = [-1]
ans = 0
heights.append(0)
lenth = len(heights)
for i in range(lenth):
while monoStack and heights[monoStack[-1]] >= heights[i]:
pos = monoStack.pop()
if monoStack:
ans = max(ans, heights[pos] * (i - monoStack[-1] - 1))
monoStack.append(i)
return ans
# write code here
- 给定节点数为 n 的二叉树的前序遍历和中序遍历结果,请重建出该二叉树并返回它的头结点。
分析:middle(lc105)
递归: 只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。(时间优化: 在中序遍历中对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置
迭代:对于前序遍历中的任意两个连续节点 u 和 v,根据前序遍历的流程,我们可以知道 u 和 v 只有两种可能的关系:
- v 是 u 的左儿子。这是因为在遍历到 u 之后,下一个遍历的节点就是 u 的左儿子,即 v;
- u 没有左儿子,并且 v 是 u 的某个祖先节点(或者 u 本身)的右儿子。如果 u 没有左儿子,那么下一个遍历的节点就是 u 的右儿子。如果 u 没有右儿子,我们就会向上回溯,直到遇到第一个有右儿子(且 u 不在它的右儿子的子树中)的节点 ua,那么 v 就是 ua 的右儿子
算法流程:- 我们用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(前序遍历的第一个节点),指针指向中序遍历的第一个节点;
- 我们依次枚举前序遍历中除了第一个节点以外的每个节点。如果 index 恰好指向栈顶节点,那么我们不断地弹出栈顶节点并向右移动 index,并将当前节点作为最后一个弹出的节点的右儿子;如果 index 和栈顶节点不同,我们将当前节点作为栈顶节点的左儿子;
- 无论是哪一种情况,我们最后都将当前的节点入栈。
class Solutio_recur:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
def myBuildTree(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int):
if preorder_left > preorder_right:
return None
# 前序遍历中的第一个节点就是根节点
preorder_root = preorder_left
# 在中序遍历中定位根节点
inorder_root = index[preorder[preorder_root]]
# 先把根节点建立出来
root = TreeNode(preorder[preorder_root])
# 得到左子树中的节点数目
size_left_subtree = inorder_root - inorder_left
# 递归地构造左子树,并连接到根节点
# 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
root.left = myBuildTree(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1)
# 递归地构造右子树,并连接到根节点
# 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
root.right = myBuildTree(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right)
return root
n = len(preorder)
# 构造哈希映射,帮助我们快速定位根节点
index = {element: i for i, element in enumerate(inorder)}
return myBuildTree(0, n - 1, 0, n - 1)
class Solution_stack:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if not preorder:
return None
root = TreeNode(preorder[0])
stack = [root]
inorderIndex = 0
for i in range(1, len(preorder)):
preorderVal = preorder[i]
node = stack[-1]
if node.val != inorder[inorderIndex]:
node.left = TreeNode(preorderVal)
stack.append(node.left)
else:
while stack and stack[-1].val == inorder[inorderIndex]:
node = stack.pop()
inorderIndex += 1
node.right = TreeNode(preorderVal)
stack.append(node.right)
return root
二、八股文
1.TCP和UDP的区别 (1, 2)
2. python中的循环引用及GIL(循环引用1, 循环引用2, GIL)
3. 简单介绍 GAN(1, 2, 3)
4. word2vec的负采样,为什么用词频来选择负样本,为什么词频越高越容易被选择成为正样本(1, 2, 3)
三、其他
- 特征选择有哪些方法?(1, 2)
- 有一个随机数发生器,以概率 P 产生0,概率 (1-P) 产生 1,请问能否利用这个随机数发生器,构造出新的发生器,以 等概率产生 0 和 1 (1)
- 二叉树的先序遍历、后序遍历能否唯一确定一棵树(1, 2)
待解决 (欢迎评论区或私信解答)
-
给出一个有序数组A和一个常数C,求所有长度为C的子序列中的最大的间距D。
一个数组的间距的定义:所有相邻两个元素中,后一个元素减去前一个元素的差值的最小值. 比如[1,4,6,9]的间距是2.
例子:A:[1,3,6,10], C:3。最大间距D应该是4,对应的一个子序列可以是[1,6,10]。 -
给定一个数字矩阵和一个数字target,比如5。从数字1开始(矩阵中可能有多个1),每次可以向上下左右选择一个方向移动一次,可以移动的条件是下个数字必须是上个数字+1,比如1必须找上下左右为2的点,2必须找上下左右为3的点,以此类推。求到达target一共有几个路径。
-
给定一个只包含0和1的字符串,判断其中有无连续的1。若有,则输出比该串大的无连续1的最小值串。若无,则不做操作
例:给定 ‘11011’ ,则输出 ‘100000’ ;给定 ‘10011’ ,则输出 ‘10100’
(参考:感觉有点像字符串匹配,只要第一次匹配到’011’模式串就改成’100’,然后后面全部置0,仅供参考) -
给定两个字符串 target 和 block,对bolck进行子串选取,选取出的子串可对target进行重构。问最少需要选取多少block子串进行重构。(子串须保持相对顺序,但不要求连续)
例:(1)target = ‘aaa’ ,block = ‘ab’ ,输出为3。即分别选取block子串中的 ‘a’、 ‘a’、 ‘a’;(2)target = ‘abcd’ ,block = ‘bcad’ ,输出为2。即分别选取子串 ‘a’、‘bcd’
(参考:双指针 i,j 分别遍历target和block,j会回溯。时间复杂度是len(target)*len(block))