高频面试题

本文最后更新于:2021年6月26日 上午

合并有序链表

参考:21 合并两个有序链表

将两个有序的链表合并为一个新链表,要求新的链表是通过拼接两个链表的节点来生成的。

输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
using namespace std;

struct myList {
int val;
myList* next;
myList(int _val) :val(_val), next(nullptr) {}
};

myList* merge(myList* l1, myList* l2) {

if (l1 == nullptr) return l2;
if (l2 == nullptr) return l1;
myList head(0);
myList* node = &head;
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
node->next = l1;
l1 = l1->next;

}
else {
node->next = l2;
l2 = l2->next;
}
node = node->next;
}

if (l1 == nullptr)
node->next = l2;
if (l2 == nullptr)
node->next = l1;

return head.next;

};

int main(void) {

myList* node0 = new myList(0);
myList* node1 = new myList(1);
myList* node2 = new myList(2);
myList* node3 = new myList(3);

myList* node4 = new myList(1);
myList* node5 = new myList(4);
node0->next = node1;
node1->next = node2;
node2->next = node3;
node3->next = nullptr;
node4->next = node5;
node5->next = nullptr;

auto node = merge(node0, node4);
while (node != nullptr) {
cout << node->val << endl;
node = node->next;
}

return 0;
}

反转链表

参考:206 反转链表92 反转链表 II25 K 个一组翻转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

输入: 1->2->3->4->5->NULL

输出: 5->4->3->2->1->NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//头插法来做,将元素开辟在栈上,这样会避免内存泄露
ListNode* ReverseList(ListNode* pHead) {
// 头插法
if (pHead == nullptr || pHead->next == nullptr) return pHead;
ListNode dummyNode = ListNode(0);
ListNode* pre = &(dummyNode);
pre->next = pHead;
ListNode* cur = pHead->next;
pHead->next = nullptr;
//pre = cur;
ListNode* temp = nullptr;
while (cur != nullptr) {
temp = cur;
cur = cur->next;
temp->next = pre->next;
pre->next = temp;
}
return dummyNode->next;
}

单例模式

  • 饿汉模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class singlePattern {
private:
singlePattern() {};
static singlePattern* p;
public:
static singlePattern* instance();

class CG {
public:
~CG() {
if (singlePattern::p != nullptr) {
delete singlePattern::p;
singlePattern::p = nullptr;
}
}
};
};

singlePattern* singlePattern::p = new singlePattern();
singlePattern* singlePattern::instance() {
return p;
}
  • 懒汉模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class singlePattern {
private:
static singlePattern* p;
singlePattern(){}
public:
static singlePattern* instance();
class CG {
public:
~CG() {
if (singlePattern::p != nullptr) {
delete singlePattern::p;
singlePattern::p = nullptr;
}
}
};
};
singlePattern* singlePattern::p = nullptr;
singlePattern* singlePattern::instance() {
if (p == nullptr) {
return new singlePattern();
}
return p;
}

简单工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
typedef enum productType {
TypeA,
TypeB,
TypeC
} productTypeTag;

class Product {

public:
virtual void show() = 0;
virtual ~Product() = 0;
};

class ProductA :public Product {
public:
void show() {
cout << "ProductA" << endl;
}
~ProductA() {
cout << "~ProductA" << endl;
}
};

class ProductB :public Product {
public:
void show() {
cout << "ProductB" << endl;
}
~ProductB() {
cout << "~ProductB" << endl;
}
};

class ProductC :public Product {
public:
void show() {
cout << "ProductC" << endl;
}
~ProductC() {
cout << "~ProductC" << endl;
}
};

class Factory {
public:
Product* createProduct(productType type) {
switch (type) {
case TypeA:
return new ProductA();
case TypeB:
return new ProductB();
case TypeC:
return new ProductC();
default:
return nullptr;
}
}
};

设计LRU缓存

参考:实现 LRU 缓存

设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。 写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class LRUCache {
list<pair<int, int>> cache;//创建双向链表
unordered_map<int, list<pair<int, int>>::iterator> map;//创建哈希表
int cap;
public:
LRUCache(int capacity) {
cap = capacity;
}

int get(int key) {
if (map.count(key) > 0){
auto temp = *map[key];
cache.erase(map[key]);
map.erase(key);
cache.push_front(temp);
map[key] = cache.begin();//映射头部
return temp.second;
}
return -1;
}

void put(int key, int value) {
if (map.count(key) > 0){
cache.erase(map[key]);
map.erase(key);
}
else if (cap == cache.size()){
auto temp = cache.back();
map.erase(temp.first);
cache.pop_back();
}
cache.push_front(pair<int, int>(key, value));
map[key] = cache.begin();//映射头部
}
};

重排链表

参考:143 重排链表

给定一个单链表 LL0→*L1→…→\Ln*-1→*Ln , 将其重新排列后变为: L0→Ln*→*L*1→*L*n\-1→*L*2→*L*n-2→…

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

  • 寻找链表中点 + 链表逆序 + 合并链表
  • 空间复杂度 O(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution {
public:
void reorderList(ListNode* head) {
if(!head || !head->next) return;
ListNode* mid = middleNode(head);
ListNode* l1 = head;
ListNode* l2 = mid->next;
mid->next = nullptr;
l2 = reverse(l2);
merge(l1, l2);
}

// 找到链表的中点
ListNode* middleNode(ListNode* head){
ListNode* slow=head, *fast=head;
while(fast->next && fast->next->next){
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 翻转链表
ListNode* reverse(ListNode* head){
if(!head || !head->next)
return head;
ListNode* rhead = reverse(head->next);
head->next->next = head;
head->next = nullptr;
return rhead;
}
// 合并链表
void merge(ListNode* l1, ListNode* l2){
ListNode* l1_tmp, *l2_tmp;
while(l1 && l2){
l1_tmp = l1->next;
l2_tmp = l2->next;

l1->next = l2;
l1 = l1_tmp;

l2->next = l1;
l2 = l2_tmp;
}
}
};

奇偶链表

参考:328 奇偶链表

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if (!(head && head->next)) return head;
ListNode* head2 = head->next;
ListNode* odd = head;
ListNode* even = head2;
while(odd->next && even->next){
odd->next = even->next;
odd = odd->next;
even->next = odd->next;
even = even->next;
}
odd->next = head2;
return head;
}
};

写三个线程交替打印ABC

参考:1195. 交替打印字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;

mutex mymutex;
condition_variable cv;
int flag=0;

void printa(){
unique_lock<mutex> lk(mymutex);
int count=0;
while(count<10){
while(flag!=0) cv.wait(lk);
cout<<"thread 1: a"<<endl;
flag=1;
cv.notify_all();
count++;
}
cout<<"my thread 1 finish"<<endl;
}
void printb(){
unique_lock<mutex> lk(mymutex);
for(int i=0;i<10;i++){
while(flag!=1) cv.wait(lk);
cout<<"thread 2: b"<<endl;
flag=2;
cv.notify_all();
}
cout<<"my thread 2 finish"<<endl;
}
void printc(){
unique_lock<mutex> lk(mymutex);
for(int i=0;i<10;i++){
while(flag!=2) cv.wait(lk);
cout<<"thread 3: c"<<endl;
flag=0;
cv.notify_all();
}
cout<<"my thread 3 finish"<<endl;
}
int main(){
thread th2(printa);
thread th1(printb);
thread th3(printc);

th1.join();
th2.join();
th3.join();
cout<<" main thread "<<endl;
}

Top K问题

解决Top K问题若干种方法

  • 使用最大最小堆。求最大的数用最小堆,求最小的数用最大堆。
  • Quick Select算法。使用类似快排的思路,根据pivot划分数组。
  • 使用排序方法,排序后再寻找top K元素。
  • 使用选择排序的思想,对前K个元素部分排序。
  • 将1000…..个数分成m组,每组寻找top K个数,得到m×K个数,在这m×k个数里面找top K个数。
  1. 使用最大最小堆的思路 (以top K 最大元素为例)

    按顺序扫描这10000个数,先取出K个元素构建一个大小为K的最小堆。每扫描到一个元素,如果这个元素大于堆顶的元素(这个堆最小的一个数),就放入堆中,并删除堆顶的元素,同时整理堆。如果这个元素小于堆顶的元素,就直接pass。最后堆中剩下的元素就是最大的前Top K个元素,最右的叶节点就是Top 第K大的元素。

    note:最小堆的插入时间复杂度为log(n),n为堆中元素个数,在这里是K。最小堆的初始化时间复杂度是nlog(n)

1
2
3
4
5
6
7
8
9
10
11
12
struct Node {
int value;
int idx;
Node (int v, int i): value(v), idx(i) {}
friend bool operator < (const struct Node &n1, const struct Node &n2) ;
};

inline bool operator < (const struct Node &n1, const struct Node &n2) {
return n1.value < n2.value;
}

priority_queue<Node> pq; // 此时pq为最大堆
  1. 使用Quick Select的思路(以寻找第K大的元素为例)

    算法的过程是这样的: 首先选取一个枢轴,然后将数组中小于该枢轴的数放到左边,大于该枢轴的数放到右边。 此时,如果左边的数组中的元素个数大于等于K,则第K大的数肯定在左边数组中,继续对左边数组执行相同操作; 如果左边的数组元素个数等于K-1,则第K大的数就是pivot; 如果左边的数组元素个数小于K,则第K大的数肯定在右边数组中,对右边数组执行相同操作。

    这个算法与快排最大的区别是,每次划分后只处理左半边或者右半边,而快排在划分后对左右半边都继续排序。

  2. 使用选择排序的思想对前K个元素排序 ( 以寻找前K大个元素为例)

    扫描一遍数组,选出最大的一个元素,然后再扫描一遍数组,找出第二大的元素,再扫描一遍数组,找出第三大的元素。。。以此类推,找K个元素,时间复杂度为O(N*K)

布隆过滤器

布隆过滤器是一个比特向量或者比特数组,它本质上是一种概率型数据结构,用来查找一个元素是否在集合中,支持高效插入和查询某条记录。常作为针对超大数据量下高效查找数据的一种方法。

它的具体工作过程是这样子的: 假设布隆过滤器的大小为m(比特向量的长度为m),有k个哈希函数,它对每个数据用这k个哈希函数计算哈希,得到k个哈希值,然后将向量中相应的位设为1。在查询某个数据是否存在的时候,对这个数据用k个哈希函数得到k个哈希值,再在比特向量中相应的位查找是否为1,如果某一个相应的位不为1,那这个数据就肯定不存在。但是如果全找到了,则这个数据有可能存在。

为什么说有可能存在呢? 因为不同的数据经过哈希后可能有相同的哈希值,在比特向量上某个位置查找到1也可能是由于某个另外的数据映射得到的。

支持删除操作吗 目前布隆过滤器只支持插入和查找操作,不支持删除操作,如果要支持删除,就要另外使用一个计数变量,每次将相应的位置为1则计数加一,删除则减一。

布隆过滤器中哈希函数的个数需要选择。如果太多则很快所有位都置为1,如果太少会容易误报。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!