语法

1
2
3
for(int e:num)  //对num中的元素进行遍历(可用于遍历字符串)。
## sort排序函数
sort(nums.begin(),nums.end());

链表

链表基本操作

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
#include <bits/stdc++.h>

using namespace std;

//定义一个结点模板
template<typename T>
struct Node {
T data;
Node *next;
Node() : next(nullptr) {}
Node(const T &d) : data(d), next(nullptr) {}
};

//删除 p 结点后面的元素
template<typename T>
void Remove(Node<T> *p) {
if (p == nullptr || p->next == nullptr) {
return;
}
auto tmp = p->next->next;
delete p->next;
p->next = tmp;
}

//在 p 结点后面插入元素
template<typename T>
void Insert(Node<T> *p, const T &data) {
auto tmp = new Node<T>(data);
tmp->next = p->next;
p->next = tmp;
}

//遍历链表
template<typename T, typename V>
void Walk(Node<T> *p, const V &vistor) {
while(p != nullptr) {
vistor(p);
p = p->next;
}
}

int main() {
auto p = new Node<int>(1);
Insert(p, 2);
int sum = 0;
Walk(p, [&sum](const Node<int> *p) -> void { sum += p->data; });
cout << sum << endl;
Remove(p);
sum = 0;
Walk(p, [&sum](const Node<int> *p) -> void { sum += p->data; });
cout << sum << endl;
return 0;
}

双指针

无法高效获取长度,无法根据偏移快速访问元素,是链表的两个劣势。然而面试的时候经常碰见诸如获取倒数第k个元素,获取中间位置的元素,判断链表是否存在环,判断环的长度等和长度与位置有关的问题。这些问题都可以通过灵活运用双指针来解决。

Tips:双指针并不是固定的公式,而是一种思维方式~

先来看”倒数第k个元素的问题”。设有两个指针 p 和 q,初始时均指向头结点。首先,先让 p 沿着 next 移动 k 次。此时,p 指向第 k+1个结点,q 指向头节点,两个指针的距离为 k 。然后,同时移动 p 和 q,直到 p 指向空,此时 q 即指向倒数第 k 个结点。可以参考下图来理解:

image-20220917105024167

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode *p = head, *q = head; //初始化
while(k--) { //将 p指针移动 k 次
p = p->next;
}
while(p != nullptr) {//同时移动,直到 p == nullptr
p = p->next;
q = q->next;
}
return q;
}
};

获取中间元素的问题。设有两个指针 fast 和 slow,初始时指向头节点。每次移动时,fast向后走两次,slow向后走一次,直到 fast 无法向后走两次。这使得在每轮移动之后。fast 和 slow 的距离就会增加一。设链表有 n 个元素,那么最多移动 n/2 轮。当 n 为奇数时,slow 恰好指向中间结点,当 n 为 偶数时,slow 恰好指向中间两个结点的靠前一个(可以考虑下如何使其指向后一个结点呢?)。

image-20220917105131443

下述代码实现了 n 为偶数时慢指针指向靠后结点。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode *p = head, *q = head;
while(q != nullptr && q->next != nullptr) {
p = p->next;
q = q->next->next;
}
return p;
}
};

判断链表是否有环

当一个链表有环时,快慢指针都会陷入环中进行无限次移动,然后变成了追及问题。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的。当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。根据上述表述得出,如果一个链表存在环,那么快慢指针必然会相遇。最后一个问题,如果存在环,如何判断环的长度呢?方法是,快慢指针相遇后继续移动,直到第二次相遇。两次相遇间的移动次数即为环的长度。

快慢指针法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow = head;//快指针
ListNode *fast = head;//满指针
while(fast != nullptr) {
fast = fast->next;//优先使用快指针遍历链表
if(fast != nullptr) {
fast = fast->next;
}
if(fast == slow) {
return true;
}
slow = slow->next;
}
return nullptr;
}
};

Morris法遍历

Morris其实解决了一个常规循环中循环到叶子节点后难以回到根节点的问题。 我们都知道前序遍历是先左后右,那么对任一节点p1来说,其右子树p1right所有节点必然在左子树p1left之后。代码中第二个while做的是,在p1left里一直往右,直到找不到更右的点,记这一点为p2。然后把p1right接到p2的右边。 这样既保证了p1right在p1left所有点之后,又不需要再回到p1节点。 即在正常的往下循环的过程中,不断把右半部分剪下来,接到左半部分的最右下。

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
class Solution {
//个人理解Morris的思想就是借助树的空闲指针将树链表化
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
while(root != null){
//前序遍历添加根节点
res.add(root.val);
//左子树不为空,按照前序遍历规则先遍历左子树
//遍历前先将左子树在前序遍历的最后一个节点指向根节点的右子树。
//前序遍历的最后一个节点:1、是叶节点,2、优先右子树,如果没有右子树再找左子树
if(root.left != null){
TreeNode precessor = root.left;
//1、是叶节点才终止循环
while(precessor.right != null || precessor.left != null){
//2、优先右子树,如果没有右子树再找左子树
if(precessor.right != null){
precessor = precessor.right;
}else{
precessor = precessor.left;
}
}
//最后一个节点指向根节点的右子树
precessor.right = root.right;
//先遍历左子树
root = root.left;
}else{
//左子树为空,按照前序遍历规则遍历右子树。
root = root.right;
}
}
return res;
}
}

BFS

1
2
3
4
5
6
7
8
queue<TreeNode*> lst;
TreeNode* tmp;
lst.push(root);
while(!lst.empty()){
tmp = lst.front();
lst.pop();
if(tmp->left) lst.push(tmp->left);
if(tmp->right) lst.push(tmp->right);}

优先队列Priority Queues

1
priority_queue <int> q; q.push(num)

全部放入后产生的队列就是排好序的队列。

哈希表

1
2
3
4
5
6
unordered_map<int, int> hashtable1;//构建哈希表

for (auto& x : hashtable2){
cout<<x.first<<':'<<x.second<<endl;
}

set

set里面每个元素只存有一个key值,它支持高效的关键字查询操作,比如检查一个关键字是否在set中。如果这个key值之前存在的话就不插入。

递归

解题思路

  1. 确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件:写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑:确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

贪心算法

「贪心算法」的问题需要满足的条件:

最优子结构:规模较大的问题的解由规模较小的子问题的解组成,规模较大的问题的解只由其中一个规模较小的子问题的解决定;
无后效性:后面阶段的求解不会修改前面阶段已经计算好的结果;
贪心选择性质:从局部最优解可以得到全局最优解。