面试算法题

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

2. 现有 n 个木条,第 i 个长度为 a[i],切割这 n 个木条得到 k 个长度为 L 的木条,L最长是多少?

二分 L,每个木条就能得到 n/L 个,计算总共能否得到至少 k 个木条。复杂度是O(n)。

3. k个有序数组怎么归并

维护一个大小为 k 的小根堆。先把每个数组第一个放入小根堆。每次把最小的取出来放入答案,并且把它所属数组的下一个放入小根堆。

const int N=205;

vector  mergeKArray(int k, vector arr[N]){
    vector ans;
    int now[N];
    priority_queue, vector >, greater > > heap;
    for(int i=0;i u=heap.top(); heap.pop();
        ans.push_back(u.first);
        int i=u.second;
        now[i]++;
        if(now[i]

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 
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_queueq;
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

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 
using namespace std;

const int N=201000;
int n,k,w[N][5],big[5];
vector >mask(1<<5);
int dp[5][1<<5];
int main(){
    scanf("%d%d",&n,&k);
    for(int i=0;i=5){
        for(int i=0;i<5;i++){
            ans+=big[i];
        }
    }else{
        for(int i=0;i

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 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(lenk]=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. 单链表上的快排

To be continued...

转载于:https://www.cnblogs.com/flipped/p/8542785.html

你可能感兴趣的:(面试算法题)