算法&&八股文&&其他
一、算法篇
-
有一个长度为 n 的按严格升序排列的整数数组 nums ,在实行 search 函数之前,在某个下标 k 上进行旋转,使数组变为
[nums[k],nums[k+1],.....,nums[nums.length-1],nums[0],nums[1],.......,nums[k-1]]
给定旋转后的数组 nums 和一个整型 target ,请你查找 target 是否存在于 nums 数组中并返回其下标(从0开始计数),如果不存在请返回-1。
分析:middle(lc33)
二分:我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:
如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [ nums[l],nums[mid] ),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
如果 [mid, r] 是有序数组,且 target 的大小满足 ( nums[mid+1],nums[r] ],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums:
return -1
l, r = 0, len(nums) - 1
while l <= r:
mid = (l + r) // 2
if nums[mid] == target:
return mid
if nums[0] <= nums[mid]:
if nums[0] <= target < nums[mid]:
r = mid - 1
else:
l = mid + 1
else:
if nums[mid] < target <= nums[len(nums) - 1]:
l = mid + 1
else:
r = mid - 1
return -1
- 给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)
分析:middle(lc102)
广度优先搜索:
- 首先根元素入队
- 当队列不为空的时候
- 求当前队列的长度 si
- 依次从队列中取 si 个元素进行拓展,然后进入下一次迭代
观察这个算法,可以归纳出这样的循环不变式:第 i 次迭代前,队列中的所有元素就是第 i 层的所有元素,并且按照从左向右的顺序排列
深度优先搜索:
主要思路:前序遍历,中、左、右
左边的节点一定先于右边节点遍历到,加入至对应的数组中,满足层序遍历的要求;
要点:
1、利用一个level变量标记当前递归的深度,将节点的值push到当前深度的数组的后面;
2、level变量大于res数组的size,说明第一次进入二叉树本层,对res扩容;
class Solution_bfs {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector <vector <int>> ret;
if (!root) {
return ret;
}
queue <TreeNode*> q;
q.push(root); //i = 1 的时候,队列里面只有 root
while (!q.empty()) {
int currentLevelSize = q.size();
ret.push_back(vector <int> ());
for (int i = 1; i <= currentLevelSize; ++i) {
auto node = q.front(); q.pop();
ret.back().push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
return ret;
}
};
//******************************************//
class Solution_dfs {
public:
/**
*
* @param root TreeNode类
* @return int整型vector<vector<>>
*/
//前序遍历模板;
void f(TreeNode* root,int level,vector<vector<int>> &res){
if(!root)return ;
if(level>=res.size()){//最新的深度,申请一个数组存储;
res.push_back(vector<int> {});
}
res[level].push_back(root->val);
f(root->left,level+1,res);//遍历左子树;
f(root->right,level+1,res);//遍历右子树;
}
vector<vector<int> > levelOrder(TreeNode* root) {
// write code here
vector<vector<int>> res;//存储最终结果;
f(root,0,res);//前序遍历;
return res;//返回结果;
}
};
- 给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false
分析:middle(lc946)
模拟:将 pushed 队列中的每个数都 push 到栈中,同时检查这个数是不是 popped 序列中下一个要 pop 的值,如果是就把它 pop 出来。
最后,检查不是所有的该 pop 出来的值都是 pop 出来了
class Solution(object):
def validateStackSequences(self, pushed, popped):
j = 0
stack = []
for x in pushed:
stack.append(x)
while stack and stack[-1] == popped[j]:
stack.pop()
j += 1
return j == len(popped)
- 给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值,整数除法仅保留整数部分。
分析:middle(lc227)
栈:由于乘除优先于加减计算,因此 不妨考虑先进行所有乘除运算,并将这些乘除运算后的整数值放回原表达式的相应位置,则随后整个表达式的值,就等于一系列整数加减后的值。
我们可以用一个栈,保存这些(进行乘除运算后的)整数的值。对于加减号后的数字,将其直接压入栈中;对于乘除号后的数字,可以直接与栈顶元素计算,并替换栈顶元素为计算后的结果。
具体来说,遍历字符串 s,并用变量 preSign 记录每个数字之前的运算符,对于第一个数字,其之前的运算符视为加号。每次遍历到数字末尾时,根据 preSign 来决定计算方式:
- 加号:将数字压入栈;
- 减号:将数字的相反数压入栈;
- 乘除号:计算数字与栈顶元素,并将栈顶元素替换为计算结果。
若读到一个运算符,或者遍历到字符串末尾,即认为是遍历到了数字末尾。处理完该数字后,更新 preSign 为当前遍历的字符。
遍历完字符串 s 后,将栈中元素累加,即为该字符串表达式的值。
class Solution:
def calculate(self, s: str) -> int:
n = len(s)
stack = []
preSign = '+'
num = 0
for i in range(n):
if s[i] != ' ' and s[i].isdigit():
num = num * 10 + ord(s[i]) - ord('0')
if i == n - 1 or s[i] in '+-*/':
if preSign == '+':
stack.append(num)
elif preSign == '-':
stack.append(-num)
elif preSign == '*':
stack.append(stack.pop() * num)
else:
stack.append(int(stack.pop() / num))
preSign = s[i]
num = 0
return sum(stack)
- 判断一个点是否在给定五角星内部
分析:判断一个点是否在多边形内部的典型方法:
- 对于一个长度为 n 字符串,我们需要对它做一些变形
首先这个字符串中包含着一些空格,就像"Hello World"一样,然后我们要做的是把这个字符串中由空格隔开的单词反序,同时反转每个字符的大小写。
比如"Hello World"变形后就变成了"wORLD hELLO"。
分析:简单题(nc89)
栈:将利用栈的先入后出特性
首先读取单词,以空格为分界符 (在字符串最后加上一个空格避免特判),读取的同时进行大小写转换,然后将单词加入到栈中。在全部单词读取完毕后,逐个弹出单词,即为答案
两次翻转:第一次将整个字符串翻转,此时每个单词所在位置即为最终位置,但每个单词中的字符顺序是翻转的。第二次将每个单词进行翻转,这样还原了每个单词内字母的原本顺序。在第二次翻转时同时对字母进行大小写转换
class Solution_stack {
public:
string trans(string s, int n) {
stack<string> sk;
string str;
s.push_back(' ');//避免特判
for(int i = 0; i <= n; ++i) {//注意此时单词长度为n+1
if(s[i] == ' ') {
sk.push(str);//以空格为界进行压栈
str = "";
} else {
if(s[i] >= 'a' && s[i] <= 'z') {
str += (s[i] - 'a' + 'A');
} else {
str += (s[i] - 'A' + 'a');
}
}
}
string ans;
while(!sk.empty()) {
//从栈中逐个弹出单词
ans += sk.top(); sk.pop();
ans.push_back(' ');
}
ans.pop_back();//去除最后一个单词后的空格
return ans;
}
};
class Solution_twoPtr {
public:
string trans(string s, int n) {
reverse(s.begin(), s.end());//将整个字符串进行翻转
int i = 0, j = 0;
while(i < n) {
j = i;
while(j < n && s[j] != ' ') {
//读取一个单词并同时进行大小写转换
if(s[j] >= 'a' && s[j] <= 'z') {
s[j] += ('A' - 'a');
} else {
s[j] += ('a' - 'A');
}
++j;
}
reverse(s.begin() + i, s.begin() + j);//翻转这个单词
i = j + 1;
}
return s;
}
};
- 给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
分析:middle(lc142)
快慢指针:我们使用两个指针,fast 与 slow。它们起始都位于链表的头部。 随后,slow 指针每次向后移动一个位置,而 fast 指针向后移动两个位置。如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇。
设链表中环外部分的长度为 a。slow 指针进入环后,又走了 b 的距离与 fast 相遇。此时,fast 指针已经走完了环的 n 圈,因此它走过的总距离为a+n(b+c)+b=a+(n+1)b+nc
任意时刻,fast 指针走过的距离都为slow 指针的 2 倍。因此,我们有
a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c)
由此发现:从相遇点到入环点的距离加上 n−1 圈的环长,恰好等于从链表头部到入环点的距离。
因此,当 slow 与 fast 相遇时,我们再额外使用一个指针 ptr。起始,它指向链表头部;随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while (fast != nullptr) {
slow = slow->next;
if (fast->next == nullptr) {
return nullptr;
}
fast = fast->next->next;
if (fast == slow) {
ListNode *ptr = head;
while (ptr != slow) {
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return nullptr;
}
};
- 搜索旋转数组。给定一个排序后的数组,包含n个整数,但这个数组已被旋转过很多次了,次数不详。请编写代码找出数组中的某个元素,假设数组元素原先是按升序排列的。若有多个相同元素,返回索引值最小的一个。
分析:middle(lc面试题 10.03)
二分:四种情况
- 左 == target: 直接返回 左
- 左 == 中: 此时target可能在[左,mid]中,也可能在[mid + 1, r]中,左 = 左 + 1
------以上两种情况需要特别注意------
- 左 < 中: 此时需要分情况讨论:
- target比左小或者target比中大时(比小的都小或者比大的都大):此时target只可能在[mid, r]中,所以 l = mid;
- 其他,即target比左大并且target比中小时(大小在左和中之间):此时target只可能在[左 + 1, mid]中,所以 l = l + 1; r = mid;
- 左 > 中: 此时需要分情况讨论:
- target比左小并且target比中大时(大小在左和中之间):此时target只可能在[mid, r]中,所以 l = mid;
- 其他,即target比左大或者比中小时(比大的都大或者比小的都小):此时target只可能在[左 + 1, mid]中,所以 l = l + 1; r = mid;
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums:
return -1
left, right = 0, len(nums) - 1
while left < right: # 循环结束条件left==right
mid = (left + right) >> 1
if nums[left] < nums[mid]: # 如果左值小于中值,说明左边区间升序
if nums[left] <= target and target <= nums[mid]: # 如果目标在左边的升序区间中,右边界移动到mid
right = mid
else: # 否则目标在右半边,左边界移动到mid+1
left = mid + 1
elif nums[left] > nums[mid]: # 如果左值大于中值,说明左边不是升序,右半边升序
if nums[left] <= target or target <= nums[mid]: # 如果目标在左边,右边界移动到mid
right = mid
else: # 否则目标在右半边的升序区间中,左边界移动到mid+1
left = mid + 1
elif nums[left] == nums[mid]: # 如果左值等于中值,可能是已经找到了目标,也可能是遇到了重复值
if nums[left] != target: # 如果左值不等于目标,说明还没找到,需要逐一清理重复值
left += 1
else: # 如果左值等于目标,说明已经找到最左边的目标值
right = left # 将右边界移动到left,循环结束
return left if nums[left] == target else -1 # 返回left,或者-1
- 给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
分析:middle(lc93)
回溯:对所有可能的字符串分隔方式进行搜索,并筛选出满足要求的作为答案
递归函数 dfs(segId,segStart) 表示我们正在从 s[segStart] 的位置开始,搜索 IP 地址中的第 segId 段,其中 segId ∈ {0,1,2,3}。 由于 IP 地址的每一段必须是 [0,255] 中的整数,因此我们从 segStart 开始,从小到大依次枚举当前这一段 IP 地址的结束位置 segEnd。如果满足要求,就递归地进行下一段搜索,调用递归函数 dfs(segId+1,segEnd+1)。
特别地,由于 IP 地址的每一段不能有前导零,因此如果 s[segStart] 等于字符 0,那么 IP 地址的第 segId 段只能为 0,需要作为特殊情况进行考虑。
在搜索的过程中,如果我们已经得到了全部的 4 段 IP 地址(即 segId=4),并且遍历完了整个字符串(即 segStart=∣s∣,其中∣s∣ 表示字符串 s 的长度),那么就复原出了一种满足题目要求的 IP 地址,将其加入答案。在其它的时刻,如果提前遍历完了整个字符串,那么我们需要结束搜索,回溯到上一步。
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
SEG_COUNT = 4
ans = list()
segments = [0] * SEG_COUNT
def dfs(segId: int, segStart: int):
# 如果找到了 4 段 IP 地址并且遍历完了字符串,那么就是一种答案
if segId == SEG_COUNT:
if segStart == len(s):
ipAddr = ".".join(str(seg) for seg in segments)
ans.append(ipAddr)
return
# 如果还没有找到 4 段 IP 地址就已经遍历完了字符串,那么提前回溯
if segStart == len(s):
return
# 由于不能有前导零,如果当前数字为 0,那么这一段 IP 地址只能为 0
if s[segStart] == "0":
segments[segId] = 0
dfs(segId + 1, segStart + 1)
# 一般情况,枚举每一种可能性并递归
addr = 0
for segEnd in range(segStart, len(s)):
addr = addr * 10 + (ord(s[segEnd]) - ord("0"))
if 0 < addr <= 0xFF:
segments[segId] = addr
dfs(segId + 1, segEnd + 1)
else:
break
dfs(0, 0)
return ans
二、八股文
1.卷积是如何实现INT8纯整型计算的 (1, 2, 3)
2. auc指标及其实现(1, 2, code)
3. 交叉熵的优缺点(1, 2)
4.xgboost原理,xgboost特征选择,如何评估特征重要性(1, 2, 3, 4)
三、其他
待解决 (欢迎评论区或私信解答)
-
给出一个有序数组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))