面试算法题
1. 给一颗多叉树,求 从一个节点出发到其它所有节点的距离之和 的最小值。
树形 dp。一般两遍 dfs 就能解决。
第一遍 dfs 用 son[i] 记录每个节点多少个子孙,用 dis[i] 记录 i 点到其所有子孙的距离之和。 son[i]和 dis[i]都在回溯的过程进行维护。假设 v 是 u 的孩子节点,\(son[u]+=son[v]+1\), \(dis[u] += dis[v]+son[v]+1\),也就是说 v 的每个子孙到 u 的距离是他们到 v 的距离+1,然后再加上 v 到 u 的距离1。
第二遍 dfs,维护 dis[i] 为到所有点的距离之和。节点 v 到其它所有节点的距离之和可以用 u 到其它所有节点的距离之和计算出来。因为v和v 的子孙到 v 的距离要比到 u 的距离少1,就减去了son[v]+1,然后剩下 n-son[v]-1个点到 v 的距离要比到 u 的距离多1,就加上了 n-son[v]-1,所以就是 \(dis[u]+n-2\times son[v]-2\)。
手写代码,大概是下面这样。
#include <bits/stdc++.h>
using namespace std;
const int N=201000;
struct edge{
int to,next;
}e[N];
int head[N],cnt;
void add_edge(int u,int v){
e[++cnt]=(edge){v,head[u]};
head[u]=cnt;
}
int son[N],dis[N];
void dfs1(int u,int fa){
for(int i=head[u];i;i=e[i].next){
int v = e[i].to;
if(v == fa)continue;
dfs1(v, u);
son[u] += son[v]+1;
dis[u] += dis[v]+son[v]+1;
}
}
int ans=1000000009;
int n,m;
void dfs2(int u,int fa){
ans=min(ans,dis[u]);
for(int i=head[u];i;i=e[i].next){
int v = e[i].to;
if(v == fa)continue;
dis[v]=dis[u]+n-2-2*son[v];
dfs2(v, u);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++){
int u,v;
scanf("%d%d",&u,&v);
add_edge(u, v);
add_edge(v, u);
}
dfs1(1, 0);
dfs2(1, 0);
printf("%d",ans);
return 0;
}
2. 现有 n 个木条,第 i 个长度为 a[i],切割这 n 个木条得到 k 个长度为 L 的木条,L最长是多少?
二分 L,每个木条就能得到 n/L 个,计算总共能否得到至少 k 个木条。复杂度是O(n)。
3. k个有序数组怎么归并
维护一个大小为 k 的小根堆。先把每个数组第一个放入小根堆。每次把最小的取出来放入答案,并且把它所属数组的下一个放入小根堆。
const int N=205;
vector <int> mergeKArray(int k, vector<int> arr[N]){
vector<int> ans;
int now[N];
priority_queue<pair<int, int>, vector<pair<int,int> >, greater<pair<int,int> > > heap;
for(int i=0;i<k;i++){
heap.push(make_pair(arr[i][0], i));
}
while(heap.size()){
pair<int,int> u=heap.top(); heap.pop();
ans.push_back(u.first);
int i=u.second;
now[i]++;
if(now[i]<arr[i].size()){
heap.push(make_pair(arr[i][now[i]],i));
}
}
return ans;
}
4. 求长度为 2n 的字典序第 k 大的合法括号序列,合法是符合下面两个要求:
1)空串
2)若 s 合法,()s、(s)也合法
思路是长度为2i的合法序列有\(2^{i-1}\)个,所以可以递归构造。如果\(2^{n-2} > k\),说明一定是()开头,再去构造长度为2(n-1)的第 k 大序列。否则,一定是(s)构成,再去构造长度为2(n-1)的第 \(k-2^{n-2}\)大的序列。
5. 平面有 n 个 x 坐标不同的点。求斜率最大的两个点。
按 x 坐标排序,然后斜率最大的两个点一定是相邻的,所以再两个两个判断一遍即可。
6. 有序数组找出出现超过一半的数。如果不知道有没有超过一半的,怎么判断。如果是判断超过1/3的呢?
中位数。用中位数的 lowerbound 和 upperbound 判断。用1/3和2/3位置的数的上下界判断。
7. 给定某单link链表,输出这个链表中倒数第k个结点。链表的倒数第0个结点为链表
的尾指针。链表结点定义如下:
struct LinkNode { int m_nKey; LinkNode* m_pNext; };
LinkNode* kthNode(LinkNode* head, int k){
LinkNode* p = head;
for(int i = 0; i < k; i++){
if (p->m_pNext == nullptr) {
return nullptr; // 不存在倒数第 k 个
}
p = p->m_pNext;
}
LinkNode* ans = head;
for(; p->m_pNext != nullptr; p = p->m_pNext) {
ans = ans->m_pNext;
}
return ans;
}
8. 给定一个二叉树中的两个节点,返回它们的最近公共祖先节点。
struct Node {
int key;
Node* Fa;
Node* Lson, *Rson;
};
Node* LeastCommonAncestors(Node* p, Node* q){
int pDep = 0, qDep = 0;
for(Node* now = p; now->Fa != nullptr; now = now->Fa) {
pDep++;
}
for(Node* now = q; now->Fa != nullptr; now = now->Fa) {
qDep++;
}
Node* nowp = p, *nowq = q;
for(; nowp != nowq; ) {
if(pDep > qDep) {
pDep--;
nowp = nowp->Fa;
}
else if(pDep < qDep){
qDep--;
nowq = nowq->Fa;
}
else(pDep == qDep){
qDep--;
pDep--;
nowp = nowp->Fa;
nowq = nowq->Fa;
}
}
return nowp;
}
9. 有 n 个节点,m 条边,求最小生成树最长的边,n,m 都是100000以内。
用小根堆优化 prim 算法。
#include <bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
const int N=201000;
int n,m;
struct edge{
int to,next,w;
}e[N];
int head[N],cnt;
void add_edge(int u,int v,int w){
e[++cnt]=(edge){v,head[u],w};
head[u]=cnt;
}
int dis[N];
bool b[N],vis[N];
struct node{
int id;
node(int v){id=v;}
bool operator < (const node&b) const{
return dis[id]>dis[b.id];
}
};
priority_queue<node>q;
int prim(){
int ans=0;
memset(dis, INF, sizeof(dis));
dis[1]=0;b[1]=true;
q.push(node(1));
while(!q.empty()){
int u=q.top().id;q.pop();
if(vis[u])continue;
vis[u]=true;
ans=max(dis[u],ans);
for(int i=head[u];i;i=e[i].next){
int v=e[i].to;
if(dis[v]>e[i].w){
dis[v]=e[i].w;
q.push(v);
}
}
}
return ans;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w);
add_edge(v,u,w);
}
printf("%d",prim());
return 0;
}
10. n 个武器,告诉你每个武器的五个属性值,现在要选择 k 个武器,最大化这五个属性的最大值之和,n,k 都是10000以内。
k>=5时,可以直接取每个属性的最大值。
k<5时,我们用mask[i]表示 i 代表的二进制位上为1的所有位置对应的属性之和最大的一个武器(用 pair 记录<最大和,编号>)。接下来用 dp[i][j]表示前 i 个已经选了 j 二进制为1的位置对应的属性时,最大是多少。那么就有 dp[i][j|m]=dp[i-1][j]+mask[i].second,其中 m和 j 在二进制相同位置不能同时为1。
#include <bits/stdc++.h>
using namespace std;
const int N=201000;
int n,k,w[N][5],big[5];
vector<pair<int,int> >mask(1<<5);
int dp[5][1<<5];
int main(){
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++){
for(int j=0;j<5;j++){
scanf("%d",&w[i][j]);
big[j]=max(big[j],w[i][j]);
}
}
int ans=0;
if(k>=5){
for(int i=0;i<5;i++){
ans+=big[i];
}
}else{
for(int i=0;i<n;i++){
for(int j=0;j<(1<<5);j++){
int tot=0;
for(int t=0;t<5;t++)
if(j&(1<<t))
tot+=w[i][t];
mask[j]=max(mask[j], make_pair(tot,i));
}
}
for(int i=1;i<=k;i++){
for(int j=0;j<(1<<5);j++){
for(int m=0;m<(1<<5);m++)if((m&j)==0){
dp[i][j|m]=max(dp[i][j|m],dp[i-1][j]+mask[m].first);
}
}
}
ans=dp[k][(1<<5)-1];
}
printf("%d",ans);
return 0;
}
11. 有20亿个无序存储的int32整数,找出一个不在里面的数。
我的方法是将int32按区间划分为10万个组,每组4万多个数。第一遍扫描,维护每组数量:对每个数计算出它所在的组,然后该组数量+1。找出数量小于区间长度的一组,在第二次扫描时,只考虑在该区间内的数,存下来。接下来将存下的数排个序,再扫一遍这个排序的数组,如果相邻的总是连续的,那么若最小的大于左边界就返回左边界否则返回右边界。如果相邻的不连续就返回中间没出现的值。
这样的时间复杂度是O(2*n+m+mlogm),n=2,000,000,000, m=int32范围/K≈40000,K=
空间复杂度是O(K+m)
在编程珠玑上看到了解法(题目类似):
二分,位图的思想。
第一次扫描,记录2进制第一位为0的个数,第一位为1的个数,
如果0开头的少,第二次扫描,就只看00,01开头的。
以此类推。要32次扫描就能确定这个数。
时间复杂度是O(32*n),空间复杂度是O(1)。实现起来也更简洁。
12. LRUCache(LeetCode146)
面试完才知道这是道经常考的面试题,有原题,只不过把键值都改为字符串。
当时写的 bug 太多了,对链表的操作也没有抽出函数来。回来又改了好久才 AC。
需要实现一个 LRU Cache 的类, 总共最多维护 n 个键值对,并具有以下两个方法:
put(k, v)
,不存在则插入,存在则更新键为 k 的值为v。
get(k)
,读取键为 k 的值,不存在输出 -1。
LRU 就是说,如果已经有 n 个了,那么再次插入时,要删去最久没有访问的。插入和读取都算是访问。
两个操作复杂度都要求是 O(1)。
插入是 O(1)的,就要求 O(1) 时间得到最久没有访问的。
我们就想维护一个队头最新,队尾最旧的队列。
如果拿双端队列,每次读取时,不能 O(1)地维护队头最新,队尾最旧的性质。
需要一个可以 O(1)地将中间的元素拿到队头,又能 O(1)地访问任何一个值的数据结构。
考虑用双向链表来存键值对,为了能 O(1)访问,再拿 hash map 来存每个键对应的链表节点的指针。
class LRUCache {
struct Node {
Node(int _k, int _v){
k = _k;
v = _v;
pre = next = NULL;
}
int k, v;
Node* pre;
Node* next;
};
int n;
unordered_map<int, Node*> m;
Node* head, *tail;
int len;
// 将 p 移到队头。
void move_to_head(Node*p){
if(p==head)return;//已经是队头。
if(head==NULL){//之前没有元素。
head=tail=p;
}else{
//如果 p 后面有元素,那么它前面的节点变成了 p 前面的节点了(也可能是 NULL)。
if(p->next) {
p->next->pre = p->pre;
}
//如果 p 前面有元素,那么它后面的节点变成了 p 后面的节点了。
if(p->pre) {
p->pre->next = p->next;
}
// 如果 p 本身就是队尾,那么新队尾是 p 前面的。这里链长一定大于1,否则 p 也会是队头,之前就返回了。
if(p==tail){
tail=p->pre;
}
// 这里就是 p 成为了新的队头。
head->pre=p;
p->next=head;
head=p;
}
}
public:
LRUCache(int _n):n(_n), len(0){
head = tail = NULL;
m.clear();
};
void reset(int _n){
len=0;
n=_n;
head = tail = NULL;
m.clear();
}
void put(int k, int v) {
if(m[k]){
move_to_head(m[k]);
m[k]->v = v;
}else {
m[k] = new Node(k, v);
if(len<n){
move_to_head(m[k]);
len++;
}else {// len == n
// 删掉最旧的。
m[tail->k]=NULL;
if(head==tail){//只有1个节点,直接删掉(n==1)
delete tail;
head=tail=NULL;
}else{//多个节点,要记得更新队尾指针。
Node*tmp=tail->pre;
delete tail;
tmp->next=NULL;
tail=tmp;
}
move_to_head(m[k]);
}
}
}
int get(int k) {
if(m[k]==NULL) return -1;
move_to_head(m[k]);
return m[k]->v;
}
};
13. 给定一个数组和滑动窗口的大小,请找出滑动窗口滑动过程的所有最大值。
双端队列
14. 单链表上的快排
15. 给一个字符串原地翻转单词
16. 单链表加法(高位在前面)
17. c[i][j] = a[i] AND b[j] 全部异或起来是多少
18. 有序数组平移一部分到后面如 4 7 9 0 1 3 ,找某个数是否存在,要求O(log n)
19. 模拟。
- 在棋盘上移动两种棋子
- 给一个dict,数字映射多个字符,将数字串生成字符串,求所有可能的字符串
- 随机选原句中某个(n个)单词并在原句中所有这个单词的下一个(n个)单词中随机选一个生成长度l的字符串。
20. 非递归版快排,递归版快排
21. 中国象棋的马能否走到目标位置,bfs。
22. k个一组翻转链表
23. 利用快排的partition找第k大的数
24. 中文数字转阿拉伯数字
25. 括号字符串判断是否有效。如果是存在分布式的几个大文件里,怎么处理。
26. 实现支持 O(1) 地 Set(k, v), Get(k), SetAll(v), Del(k) 的数据结构
To be continued...
┆凉┆暖┆降┆等┆幸┆我┆我┆里┆将┆ ┆可┆有┆谦┆戮┆那┆ ┆大┆始┆ ┆然┆
┆薄┆一┆临┆你┆的┆还┆没┆ ┆来┆ ┆是┆来┆逊┆没┆些┆ ┆雁┆终┆ ┆而┆
┆ ┆暖┆ ┆如┆地┆站┆有┆ ┆也┆ ┆我┆ ┆的┆有┆精┆ ┆也┆没┆ ┆你┆
┆ ┆这┆ ┆试┆方┆在┆逃┆ ┆会┆ ┆在┆ ┆清┆来┆准┆ ┆没┆有┆ ┆没┆
┆ ┆生┆ ┆探┆ ┆最┆避┆ ┆在┆ ┆这┆ ┆晨┆ ┆的┆ ┆有┆来┆ ┆有┆
┆ ┆之┆ ┆般┆ ┆不┆ ┆ ┆这┆ ┆里┆ ┆没┆ ┆杀┆ ┆来┆ ┆ ┆来┆