面试算法题

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...

posted @ 2018-03-10 23:45  水郁  阅读(456)  评论(0编辑  收藏  举报
……