目录

常用算法笔记

回溯

思想

回溯就是使用暴力搜索,利用递归和for循环返回所有满足需求的答案,可以用于解决组合、切割、子集、排列、N皇后等问题。回溯发生在递归时,处理完一种情况后需要在答案中撤销当前情况,便于加入以后的数据得到新的答案。

模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class backtracking {
private:
	全局变量 ans 用于保存满足要求的答案
public:
	// 递归调用该函数进行暴力搜索并进行回溯操作
	void dfs(参数) {
		if (终止条件) {
			收集结果到ans中保存
			return;
		}
		for (集合元素) {
			处理结点
			递归调用dfs()
			*回溯操作*
		}
		return;
	}
};

例题

93. 复原 IP 地址

306. 累加数

二分查找

二分搜索高效的本质在于每次排除可能的空间比顺序遍历更大。在一个有序排列中,首先应想到用二分查找。

标准二分查找模板

左闭右闭

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class BinarySearch {
public:
	int search(vector<int>& nums, int target) {
		int left = 0;
		int right = nums.size() - 1;
		while (left <= right) {
			int mid = left + ((right - left) >> 1);
			if (nums[mid] == target)
                return mid;
            if (nums[mid] > target)
				right = mid - 1;
			else if (nums[mid] < target)
				left = mid + 1;
		}
		return -1;
	}
};
  • 查找范围为[left, right],为闭区间;

  • 循环条件:left <= right,需要包括相等的情况;

  • 右边界更新:right = mid - 1,左边界更新:left = mid + 1

  • left + ((right -left) >> 1) 对于目标区域长度为奇数而言,是处于正中间的,对于长度为偶数而言,是中间偏左的。因此左右边界相遇时,只会是以下两种情况:

    • left/midright(left,mid指向同一个数,right指向它的下一个数);
    • left/mid/right(left,mid,right指向同一个数)。
  • 命中时,会有以下三种情况:

/img/common-algorithms/2.png

  • 未命中时,考虑以下两种边界情况:

/img/common-algorithms/6.png

可以得到以下两种结果:

/img/common-algorithms/3.png

左闭右开

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class BinarySearch {
public:
	int search(vector<int>& nums, int target) {
		int left = 0;
		int right = nums.size();
		while (left < right) {
			int mid = left + ((right - left) >> 1);
			if (nums[mid] == target)
                return mid;
            if (nums[mid] > target)
				right = mid;
			else if (nums[mid] < target)
				left = mid + 1;
		}
		return -1;
	}
};
  • 查找范围为[left, right),右边为开区间;
  • 循环条件:left < right,不包括相等的情况;
  • 右边界更新:right = mid,左边界更新:left = mid + 1
  • 命中时,会有以下两种情况:

/img/common-algorithms/4.png

  • 未命中时,考虑以下四种边界情况:

/img/common-algorithms/7.png

可以得到以下两种结果:

/img/common-algorithms/5.png

例题

875. 爱吃香蕉的珂珂

2064. 分配给商店的最多商品的最小值

1414. 和为 K 的最少斐波那契数字数目

字典树/前缀树/Trie

/img/common-algorithms/1.png

模板

定义类Trie

1
2
3
4
5
6
7
8
class Trie {
private:
	bool isEnd;
	vector<Trie*> next;
public:
	Trie() : isEnd(false), next(26) {}
	//方法将在下文实现...
};

插入

描述:向Trie中插入一个单词word。

实现:这个操作和构建链表很像。首先从根结点的子结点开始与word第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完word的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void insert(string word) {
	Trie* node = this;
	for (auto &ch : word) {
		if (!node->next[ch-'a']) {
			node->next[ch-'a'] = new Trie();
		}
		node = node->next[ch-'a'];
	}
	node->isEnd = true;
}

查找

描述:查找Trie中是否存在单词word。

实现:从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回false,如果匹配到了最后一个字符,那我们只需判断node->isEnd即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bool search(string word) {
	Trie* node = this;
	for (auto &ch : word) {
		node = node->next[ch-'a'];
		if (!node) {
			return false;
		}
	}
	return node->isEnd;
}

例题

208. 实现 Trie (前缀树)

211. 添加与搜索单词 - 数据结构设计

并查集

并查集(英文:Disjoint-set data structure,直译为不交集数据结构)是一种数据结构,用于处理一些不交集(Disjoint sets,一系列没有重复元素的集合)的合并及查询问题。并查集支持如下操作:

  • 查询:查询某个元素属于哪个集合,通常是返回集合内的一个“代表元素”。这个操作是为了判断两个元素是否在同一个集合之中。

  • 合并:将两个集合合并为一个。

  • 添加:添加一个新集合,其中有一个新元素。添加操作不如查询和合并操作重要,常常被忽略。

由于支持查询和合并这两种操作,并查集在英文中也被称为联合-查找数据结构(Union-find data structure)或者合并-查找集合(Merge-find set)。

模板

 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
class UnionFind {
public:
	vector<int> parent;
	vector<int> ranker;

	UnionFind(int _n): parent(_n), ranker(_n, 1) {
		iota(parent.begin(), parent.end(), 0);
	}
	
	int find(int x) {
		if (parent[x] == x)
			return x;
		parent[x] = find(parent[x]);
		return parent[x];
	}
    
	bool unionElement(int x, int y) {
		int fx = find(x), fy = find(y);
		if (fx == fy)
			return false;
		if (ranker[fx] < ranker[fy])
			swap(fx, fy);
		parent[fy] = fx;
		if (ranker[fx] == ranker[fy])
			++ranker[fx];
		return true;
	}
    
	bool isConnected(int x, int y) {
		int fx = find(x), fy = find(y);
		return fx == fy;
	}
    
	bool disConnect(int x) {
		parent[x] = x;
		return true;
	}
};

例题

5941. 找出知晓秘密的所有专家

随机化快速排序

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。通过随机化的快排,选取 nums[begin]nums[end] 中的随机一个元素作为主元,然后再进行划分,就可以得到一个平衡的划分,它的平均运行时间是$O(nlogn)$。

模板

 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
class QuickSort {
private:
	void sort(vector<int>& nums, int begin, int end) {
		if (begin >= end)
			return;
        // 随机选取一个元素与首元素进行交换,作为pivot
		srand((unsigned)time(NULL));
		int a = begin + rand() % (end - begin);
		swap(nums, a, begin);
		int i = begin, j = end;
		int pivot = nums[i];
		while (i < j) {
			while (i < j && nums[j] >= pivot)
				--j;
			if (i < j) {
				nums[i] = nums[j];
				++i;
			}
			while (i < j && nums[i] <= pivot)
				++i;
			if (i < j) {
				nums[j] = nums[i];
				--j;
			}
		}
		nums[i] = pivot;
		quickSort(nums, i + 1, end);
		quickSort(nums, begin, i - 1);
		return 0;
	}

	void swap(vector<int>& nums, int a, int b) {
		int temp = nums[a];
		nums[a] = nums[b];
		nums[b] = temp;
	}
};

例题

215. 数组中的第K个最大元素