算法&&八股文&&其他
一、算法篇
- 给出一个升序排序的链表,删除链表中的所有重复出现的元素,只保留原链表中只出现一次的元素
分析:middle(lc82)
双指针:首先我们可以设置一个newhead对象,pre = newhead,cur = head,在已知head的情况下,我们开始循环判断cur对象,设置循环条件为cur ->next不为空,判断cur->val是否等于cur->next->val
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
# @param head ListNode类
# @return ListNode类
#
class Solution:
def deleteDuplicates(self , head: ListNode) -> ListNode:
tmpNode = ListNode(-1) #虚拟头节点
preNode = tmpNode #记录当前节点前驱
cur = head
preNode.next = cur
while cur: #遍历链表
if cur.next and cur.val == cur.next.val: #判断是否重复
while cur.next and cur.val == cur.next.val:
cur = cur.next
cur = cur.next #找到第一个与当前节点不相等的节点作为新的当前节点
preNode.next = cur #将前驱节点的后继更新为
else:
preNode = preNode.next #若不重复,直接更新前驱与当前节点
cur = cur.next
return tmpNode.next
# write code here
- 先/中/后序遍历(迭代和递归两种方式)
分析:简单(nc45)
递归 :
「二叉树的先序遍历」的思路是:先访问根结点,再访问左子树,最后访问右子树;
「二叉树的中序遍历」的思路是:先访问左子树,再访问根结点,最后访问右子树;
「二叉树的后序遍历」的思路是:先访问左子树,再访问右子树,最后访问根结点;迭代:
先序遍历步骤如下:
1.根结点入栈;
重复如下操作:
i.访问栈顶结点
ii.右孩子入栈(若有)
iii.左孩子入栈(若有)
中序遍历过程步骤如下:
i.访问以当前结点p为根结点的「最左孩子」n;
ii.访问n,并将p更新至其右子树上:p = p->right;
iii.若p无右子树,则说明p是某棵子树的根结点,则在下一次循环中访问p;若p有右子树,则按照上述相同步骤访问该右子树,最后访问p。
后序遍历过程步骤如下:
1. 定义last指针,用以代表「上次访问的位置」,p结点用来遍历;
2. 由于后续遍历需要先访问左子树和右子树、最后访问根结点,因此在满足如下条件时才能访问该结点:
i. 该结点没有右子树;
ii. 该结点有右子树,且上次访问的结点(last指针)为其右子树,即该右子树已经被访问过了;
3. 在不满足上述条件的情况下,说明当前结点还不能被访问,故先访问其右子树:p = p->right;
4. 将p结点置为NULL,防止下次while循环时重复访问;
/**
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
//递归
class Solution_recursion {
public:
/**
*
* @param root TreeNode类 the root of binary tree
* @return int整型vector<vector<>>
*/
vector<vector<int> > res;
vector<int> preorder, inorder, postorder;
vector<vector<int> > threeOrders(TreeNode* root) {
preOrder(root);
inOrder(root);
postOrder(root);
res.push_back(preorder);
res.push_back(inorder);
res.push_back(postorder);
return res;
}
void preOrder(TreeNode* root) {
if (!root) return;
preorder.push_back(root->val); // 访问根结点
preOrder(root->left); // 访问左子树
preOrder(root->right); // 访问右子树
}
void inOrder(TreeNode* root) {
if (!root) return;
inOrder(root->left); // 访问左子树
inorder.push_back(root->val); // 访问根结点
inOrder(root->right); // 访问右子树
}
void postOrder(TreeNode* root) {
if (!root) return;
postOrder(root->left); // 访问左子树
postOrder(root->right); // 访问右子树
postorder.push_back(root->val); // 访问根结点
}
};
//迭代
class Solution_iter {
public:
/**
*
* @param root TreeNode类 the root of binary tree
* @return int整型vector<vector<>>
*/
vector<vector<int> > res;
vector<int> preorder, inorder, postorder;
vector<vector<int> > threeOrders(TreeNode* root) {
preOrder(root);
inOrder(root);
postOrder(root);
res.push_back(preorder);
res.push_back(inorder);
res.push_back(postorder);
return res;
}
void preOrder(TreeNode* root) {
if (!root) return;
stack<TreeNode*> s;
TreeNode* p = root;
s.push(p);
while (s.size()) {
p = s.top(); // 访问根结点
s.pop(); // 根结点出栈
preorder.push_back(p->val);
if (p->right) s.push(p->right); // 右子树入栈
if (p->left) s.push(p->left); // 左子树入栈
}
return;
}
void inOrder(TreeNode* root) {
if (!root) return;
stack<TreeNode*> s;
TreeNode* p = root;
while (s.size() || p) {
while (p) { // 寻找最左的孩子
s.push(p);
p = p->left;
}
p = s.top();
inorder.push_back(p->val); // 访问
s.pop();
p = p->right; // 右子树
}
return;
}
void postOrder(TreeNode* root) {
if (!root) return;
stack<TreeNode*> s;
TreeNode* p = root, *last = NULL; // last记录先前被访问的结点
while (s.size() || p) {
while (p) { // 最左的孩子
s.push(p);
p = p->left;
}
p = s.top();
if (!p->right || last == p->right) { // 若该结点没有右孩子,或上一次访问的是右子树,则直接访问该结点
s.pop();
postorder.push_back(p->val);
last = p; // 更新last
p = NULL; // p置空,防止下一次循环重复访问
} else {
p = p->right; // 右子树
}
}
}
};
- 有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
分析:简单(nc71)
二分:
算法流程:
1. 初始化: 声明 i, j 双指针分别指向 array 数组左右两端
2. 循环二分: 设 m = (i + j) / 2 为每次二分的中点( “/” 代表向下取整除法,因此恒有 i≤m)
i. 当 array[m] > array[j] 时: m 一定在 左排序数组 中,即旋转点 x 一定在 [m + 1, j] 闭区间内,因此执行 i = m + 1
ii. 当 array[m] < array[j] 时: m 一定在 右排序数组 中,即旋转点 x 一定在[i, m]闭区间内,因此执行 j = m
iii. 当 array[m] = array[j] 时: 无法判断 m 在哪个排序数组中,即无法判断旋转点 x 在 [i, m] 还是 [m + 1, j] 区间中。解决方案: 执行 j = j - 1 缩小判断范围
3. 返回值: 当 i = j 时跳出二分循环,并返回 旋转点的值 array[i] 即可。
# -*- coding:utf-8 -*-
class Solution:
def minNumberInRotateArray(self, rotateArray):
# write code here
# 特殊情况判断
if len(rotateArray) == 0:
return 0
# 左右指针
i, j = 0, len(rotateArray) - 1
while i < j:
# 确定中点索引
m = (i + j) // 2
# m在左排序数组中,旋转点在 [m+1, j] 中
if rotateArray[m] > rotateArray[j]: i = m + 1
# m在右排序数组中,旋转点在 [i, m] 中
elif rotateArray[m] < rotateArray[j]: j = m
else: j -= 1
# 返回结果
return rotateArray[i]
- 给出一组可能包含重复项的数字,返回该组数字的所有排列。结果以字典序升序排列。
分析:middle(nc42)
三种方法:
回溯——基于辅助数组
回溯——基于交换
基于c++库函数next_permutation
#include <vector>
#include <algorithm>
using namespace std;
//法一
class Solution {
public:
vector<vector<int> > permuteUnique(vector<int> &num) {
vector<vector<int> > res;
//visited数组在标记当前元素是否已经被选择过了,从而避免得到重复的排列
vector<int> visited(num.size(), 0);
//因为存在重复项,为了更好的处理,将数组先排序
sort(num.begin(), num.end());
dfs(res, num, visited, vector<int>());
return res;
}
void dfs(vector<vector<int> > &res, vector<int> &num, vector<int> &visited, vector<int> vec) {
if (vec.size() == num.size()) { res.push_back(vec); return; }
for (int i = 0; i < num.size(); ++i) {
if (visited[i] == 1) continue;
//举例 112
if (i>0 && num[i-1]==num[i] && !visited[i-1]) continue;
visited[i] = 1;
vec.push_back(num[i]);
dfs(res, num, visited, vec);
vec.pop_back();
visited[i] = 0;
}
}
};
//法二
class Solution {
public:
vector<vector<int> > permuteUnique(vector<int> &num) {
vector<vector<int> > res;
set<vector<int>> st;
dfs(st, num, 0);
for (auto vec : st) res.push_back(vec);
return res;
}
void dfs(set<vector<int> > &st, vector<int> &num, int idx) {
if (idx == num.size()-1) { st.insert(num); return; }
for (int i = idx; i < num.size(); ++i) {
if (i != idx && num[i] == num[idx]) continue; // 剪枝
swap(num[i], num[idx]);
dfs(st, num, idx+1);
swap(num[i], num[idx]);
}
}
};
//法三
class Solution {
public:
vector<vector<int> > permuteUnique(vector<int> &num) {
vector<vector<int> > res;
sort(num.begin(), num.end());
do{
res.push_back(num);
} while (next_permutation(num.begin(), num.end()));
return res;
}
};
- 给定一个 n * m 的矩阵 a,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,输出所有的路径中最小的路径和。
分析:middle(nc285)
使用一个二维数组dp,dp[i][j]表示的是从左上角到坐标(i,j)的最小路径和。那么走到坐标(i,j)的位置只有这两种可能,要么从上面(i-1,j)走下来,要么从左边(i,j-1)走过来,我们要选择路径和最小的再加上当前坐标的值就是到坐标
(i,j)的最小路径。
# @param matrix int整型二维数组 the matrix
# @return int整型
#
class Solution:
def minPathSum(self , matrix ):
n = len(matrix)
m = len(matrix[0])
for r in range(1, n): #直接在原数组上更新
matrix[r][0] = matrix[r-1][0] + matrix[r][0]
for c in range(1, m):
matrix[0][c] = matrix[0][c-1] + matrix[0][c]
for r in range(1, n):
for c in range(1, m):
matrix[r][c] = min(matrix[r-1][c], matrix[r][c-1]) + matrix[r][c]
return matrix[-1][-1]
# write code here
二、八股文
- ID3,C4.5,CART决策树 (1, 2)
- 感受野计算(1, 2)
- 矩阵分解有哪几种(1, 2, 3, 4)
- c++数组和vector容器的区别(1, 2, 3)
- BNNeck介绍(1)
- kdTree算法和KNN算法(1, 2, 3, 4)
- 注意力机制的算法复杂度(1)
- ADAM和ADAMW优化器(1, 2)
- BERT的激活函数GELU(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一共有几个路径。