一、搜索和查找
无论是对于现实世界还是计算机世界,对于指定目标的查找,始终都是一个重要的问题。这里面藏着一个容易忽视的问题即小数据量查找时,几乎大多数的方法都可以实现相关的目标,即使有一些效率的损失,也多在可承受的范围内。可是,一旦数据到达了一定的量级,不同的查找算法则可能在不同的场景下产生巨大的差异。
随着计算机技术应用的广泛普及,特别是移动互联网和AI的暴发带来了海量数据的处理。在这种情况下,搜索和查找算法的高效设计,往往能够产生极大的效率提升。
举一个例子,如果有一座图书馆,如何保存书籍可以让借阅者迅速的找到相关的书籍呢?聪明的人可能想到了将书籍名称按拼音字母排序存储,也有人可能想到了按书籍的种类划分,还有的想到了按出版时间划分…这些都是不错的方法,都是用来查找和搜索的有效的手段。但可能在特定的场景中还是会分出优劣?所以在实际的应用中,往往是多种方式共同进行处理。
二、分类
搜索和查找的算法从不同的角度划分其实有多种分类。这里主要从两种角度来划分:
- 从搜索查找思想上划分
这种角度来划分搜索和查找有两大类:
暴力遍历:这是最容易理解的一种,从头到尾遍历集合中的每个对象找到符合条件的元素对象
自适应搜索:利用数据自身的特点(典型的就是排好序的数据集合)来通过算法优化搜索,它可能会引入额外的空间需求和数据预处理。搜索效率相对高 - 从数据类型分
如果从数据类型分可分为三类:
数据查找:一般是指对线性表、数组、树及哈希表等数据结构中查找元素对象
图或状态空间查找:主要是指在图、树或路径等中查找节点或最优解
字符串查找:在文本中查找指定的子字符串或符合条件的模式串
从宏观上掌握搜索和查找是做什么的?面对的目标和实现的结果是什么?就会有的放矢的去确定如何使用正确的搜索和查找算法。
三、算法分析
一般来说,常见的搜索和查找的算法主要有以下几种:
-
顺序查找
顺序查找也叫线性查找,它很简单,就是从头到尾逐个进行比较,直到找到指定目标或遍历完成即可。它的时间复杂度最好是O(1),最坏是O(n),平衡是O(n),其空间复杂度是O(1)。
顺序查找是一种最直观、简单的查找方式,它的适用性最强,它对查找的数据没有要求。但其线性增长的时间复杂度,并不适合海量数据的处理 -
二分(分块)查找
二分是指把查找集分成两块,但它要求数据是排序之后的。它是一种高效的搜索查找算法,通过不断的将查找数据空间减半来确定查找目标。其方式一般是比较搜索集中间元素对象与目标值,匹配则找到返回;否则,目标值小于中间元素,则在大于一侧的半集合内继续搜索;目标值大于中间元素,则在小于侧的半集合内继续搜索。如此迭代并最终找到目标或搜索结束。
分块是一种相对二分更宽松一些的查找,它在块间要求排序,但块内不要求。它和分块查找其实都属于分治法的范畴。它在块间可按类似二分的机制进行块的定位,在块内可以使用顺序查找的方式(如果已排序可继续使用分块或二分查找)。
二分查找的时间复杂度是O(log n),空间复杂度是O(1)。二分查找主要适应于排序好的数据集,所以它需要数据预处理,而排序往往也是一个很耗费时间或空间的过程。但二分查找对于大数据搜索有着很大的优势 -
哈希查找
哈希从某种角度看也是一种分块查找,只是它将块分得更细。作为一种快速查找技术,利用哈希函数将Key转换为索引并可以通过索引直接访问数据Value。它的查找速度与数据量大小无关,所以查询效率相当高。
它一般要先确定一个哈希函数,计算相关Key的哈希地址;然后确定该地址是否存在冲突(是否已有数据);如果没有直接存储,如果有,则使用链表、再哈希等方式解决冲突。
理论上来讲,哈希表的时间复杂度为O(1),但如果存在着较多的冲突,则可能有所变化,最坏可能为O(n);其空间复杂度为O(n)。哈希表的在海量数据搜索和查找中应用非常广泛,比如有名的布隆过滤器和布谷鸟过滤器。但使用哈希表有一定的要求,需要处理好哈希函数的选择,否则可能会达不到预定的设想 -
树查找
一般是指二叉搜索树BST,这种树有一定的要求即左子树的节点值都小于根根节点值,而右子树节点值都大于根节点值。查找时会根据当前的节点值与目标值比较来判断进入左子树或右子树。它和二分法一样同属于分治法的一类。
其具体的查找过程是从根节点开始匹配,比较目标值与当前节点值,成功则返回;如果目标值大则进入右子树,否则进入左子树。如此迭代,直到查找到指定的目标或搜索完成。
树查找的时间复杂度为O(log n),最坏则退化为链表为O(n);其空间复杂度为O(1)。树查找适合于一些复杂的情况和数据量大的情况,如数据库就是树的典型应用。由于树本身的操作和管理也相对复杂,所以一般对开发者的要求相对较高 -
深度优先查找
DFS,按俗话来说就是“一根筋”,它会沿着一条路径尽可能的深入,直到末尾才会进行回溯。可以简单理解为,遍历完成一“列”,再遍历另外一“列”,直到全部遍历完成(比如探索一栋楼,按单元搜索)。它一般借助栈来实现通过递归的方式,实现对数据集合的遍历
其时间复杂度为O(V + E),V为顶点数,E是边数。空间复杂度为O(V)。它和广度优先查找一般适合于图、树或迷宫等问题的解决 -
广度优先查找
BFS,从起始节点开始,先访问距离最近(距离为1)的节点,依次再访问次之(距离为2,3…)的节点。可以理解为按“行”或“层”来遍历,直到遍历完成所有集合数据(比如探索一栋楼,按层搜索)。它一般是使用队列来进行实现。
其时间复杂度为O(V + E),V为顶点数,E是边数。空间复杂度为O(V) -
字符串查找
Knuth-Morris-Pratt,其原理在于利用已经匹配过的部分信息,避免主串指针的回退。KMP通过预处理模式串,构建一个前缀函数的部分匹配表,通过匹配的结果来动态确定整体的滑动位数和不可能匹配成功的区间,同时保持主串指针的位置不变。
KMP专注在一个长文本(主串)中定位一个特定的子串(模式串)。它的优点是将字符串匹配的时间复杂度从“暴力回溯”的 O(n*m) 降到了理论最优的线性级别。
其时间复杂度为O(n + m),其中 n 是主串长度,m 是模式串长度。空间复杂度为O(m)。它一般适合于长文本中字符串的匹配防止进行全域的遍历 -
A查找
A-star Search,这种查找属于一种启发式搜索。它会利用估价函数来智能的决定先搜索哪个节点。其核心公式为:F(n) = G(n) + H(n)
G(n):起点到当前节点n的实际代价(已经走过的路)
H(n):从当前节点n到目标节点的估计代价(预估还要走的路,如曼哈顿距离、欧几里得距离)
F(n):综合评价。A在每一步都优先探索F(n)值最小的节点(使用优先队列)
其时间复杂度最坏为 O(b^d),b 是分支因子,d 是目标深度。空间复杂度为O(b^d)。其要求当H(n)满足可采纳性(Admissible)(即估计值 ≤ 实际值)时,A*找到的第一条路径必定是全局最优最短路径。
这种算法一般接触的比较少,主要应用于游戏和路径规则以及地图搜索中
其实算法是死的,但应用是灵活的,这就看解决问题的设计者的思想水平了。
四、应用
正如前面分析,哪种搜索和查找的算法,都各自有各自的优势,有时候甚至一种算法达不到目的。就可以使用多搜索算法结合,逐层分块推进来解决问题。下面给出一些常见的选择的建议:
- 数据规模不大且未排序,数据可能不断更新,查找特定目标,可考虑顺序查找
- 数据规模较大但已排序,数据无更新,查找特定目标,可考虑使用二分查找
- 如果对空间不太敏感,查询速度要求很高且数据无序,查找特定目标,可考虑哈希查找
- 如果数据空间很大,需要不断的动态维护(如数据库)和范围查询,可考虑使用二叉搜索树(如AVL或红黑树等)
- 如果需要对路径、状态或图进行处理,可考虑使用DFS、BFS、A*查找
- 对于字符串查找,一般建议使用KMP查找方式
要善于利于工程组合进行创新,不要拘泥于某个算法僵化、教条的使用它们。目的只有一个,高效、快捷的搜索和查找到指定目标。
五、例程
下面给出相对应的例程:
//顺序表查找
#include <iostream>
#include <vector>
int linearSearch(const std::vector<int>& arr, int target) {
for (int i = 0; i < static_cast<int>(arr.size()); ++i) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
int main() {
std::vector<int> arr = {6, 3, 9, 0, 7};
int index = linearSearch(arr, 9);
return 0;
}
//二分查找
#include <iostream>
#include <vector>
int binarySearch(const std::vector<int>& arr, int target) {
int left = 0;
int right = static_cast<int>(arr.size()) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
}
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
int main() {
std::vector<int> arr = {3, 12, 5, 6, 11};
std::cout << binarySearch(arr, 6) << std::endl;
return 0;
}
//哈希查找可参看STL中的std::unordered_map的find函数
//二叉搜索树
#include <iostream>
struct Node {
int value;
Node* left;
Node* right;
explicit Node(int v) : value(v), left(nullptr), right(nullptr) {}
};
Node* insert(Node* root, int value) {
if (root == nullptr) {
return new Node(value);
}
if (value < root->value) {
root->left = insert(root->left, value);
} else if (value > root->value) {
root->right = insert(root->right, value);
}
return root;
}
bool search(Node* root, int target) {
if (root == nullptr) {
return false;
}
if (root->value == target) {
return true;
}
if (target < root->value) {
return search(root->left, target);
}
return search(root->right, target);
}
int main() {
Node* root = nullptr;
root = insert(root, 9);
root = insert(root, 3);
root = insert(root, 10);
root = insert(root, 2);
root = insert(root, 6);
std::cout << std::boolalpha << search(root, 3) << std::endl;
std::cout << std::boolalpha << search(root, 10) << std::endl;
return 0;
}
//DFS,深度优先
#include <iostream>
#include <vector>
void dfs(int u, const std::vector<std::vector<int>>& graph, std::vector<bool>& visited) {
visited[u] = true;
for (int v : graph[u]) {
if (!visited[v]) {
dfs(v, graph, visited);
}
}
}
int main() {
std::vector<std::vector<int>> graph = {
{1, 2},{0, 3, 4},{0},{1},{1}
};
std::vector<bool> visited(graph.size(), false);
dfs(0, graph, visited);
return 0;
}
//KMP
#include <iostream>
#include <string>
#include <vector>
std::vector<int> buildNext(const std::string& pattern) {
std::vector<int> next(pattern.size(), 0);
int j = 0;
for (int i = 1; i < static_cast<int>(pattern.size()); ++i) {
while (j > 0 && pattern[i] != pattern[j]) {
j = next[j - 1];
}
if (pattern[i] == pattern[j]) {
++j;
}
next[i] = j;
}
return next;
}
int kmpSearch(const std::string& text, const std::string& pattern) {
if (pattern.empty()) {
return 0;
}
std::vector<int> next = buildNext(pattern);
int j = 0;
for (int i = 0; i < static_cast<int>(text.size()); ++i) {
while (j > 0 && text[i] != pattern[j]) {
j = next[j - 1];
}
if (text[i] == pattern[j]) {
++j;
}
if (j == static_cast<int>(pattern.size())) {
return i - j + 1;
}
}
return -1;
}
int main() {
std::string text = "aababcabcacbabd";
std::string pattern = "abcac";
std::cout << kmpSearch(text, pattern) << std::endl;
return 0;
}
//A*搜索
#include <cmath>
#include <iostream>
#include <queue>
#include <tuple>
#include <vector>
struct Node {
int x;
int y;
int g;
int f;
bool operator>(const Node& other) const {
return f > other.f;
}
};
int heuristic(int x1, int y1, int x2, int y2) {
return std::abs(x1 - x2) + std::abs(y1 - y2);
}
bool inBounds(int x, int y, int rows, int cols) {
return x >= 0 && x < rows && y >= 0 && y < cols;
}
int aStar(const std::vector<std::vector<int>>& grid,
int sx, int sy,
int tx, int ty) {
int rows = grid.size();
int cols = grid[0].size();
std::vector<std::vector<int>> dist(rows, std::vector<int>(cols, 1e9));
std::priority_queue<Node, std::vector<Node>, std::greater<Node>> pq;
dist[sx][sy] = 0;
pq.push({sx, sy, 0, heuristic(sx, sy, tx, ty)});
std::vector<std::pair<int, int>> dirs = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
while (!pq.empty()) {
Node cur = pq.top();
pq.pop();
if (cur.x == tx && cur.y == ty) {
return cur.g;
}
for (auto [dx, dy] : dirs) {
int nx = cur.x + dx;
int ny = cur.y + dy;
if (!inBounds(nx, ny, rows, cols)) {
continue;
}
if (grid[nx][ny] == 1) {
continue;
}
int newG = cur.g + 1;
if (newG < dist[nx][ny]) {
dist[nx][ny] = newG;
int newF = newG + heuristic(nx, ny, tx, ty);
pq.push({nx, ny, newG, newF});
}
}
}
return -1;
}
int main() {
std::vector<std::vector<int>> grid = {
{0, 0, 0, 0},
{1, 1, 0, 1},
{0, 0, 0, 0},
{0, 1, 1, 0}
};
int steps = aStar(grid, 0, 0, 3, 3);
std::cout << "shortest steps = " << steps << std::endl;
return 0;
}
广度遍历和深度遍历类似就不再给出相应的代码。
六、总结
搜索和查找是互联网和工程开发中最常见的一种应用。它的应用场景多种多样,数据大小、类别等也是各有不同。这就需要开发者在应用时要把握住特点和应用限制,实现设计的目标。不过对于大多数的应用来说,数据量都不大,所以更多的情况是开发者使用各种库提供的基础搜索和遍历都能达到目标。
数据结构和算法—常见的底层查找算法
一、搜索和查找
无论是对于现实世界还是计算机世界,对于指定目标的查找,始终都是一个重要的问题。这里面藏着一个容易忽视的问题即小数据量查找时,几乎大多数的方法都可以实现相关的目标,即使有一些效率的损失,也多在可承受的范围内。可是,一旦数据到达了一定的量级,不同的查找算法则可能在不同的场景下产生巨大的差异。
随着计算机技术应用的广泛普及,特别是移动互联网和AI的暴发带来了海量数据的处理。在这种情况下,搜索和查找算法的高效设计,往往能够产生极大的效率提升。
举一个例子,如果有一座图书馆,如何保存书籍可以让借阅者迅速的找到相关的书籍呢?聪明的人可能想到了将书籍名称按拼音字母排序存储,也有人可能想到了按书籍的种类划分,还有的想到了按出版时间划分…这些都是不错的方法,都是用来查找和搜索的有效的手段。但可能在特定的场景中还是会分出优劣?所以在实际的应用中,往往是多种方式共同进行处理。
二、分类
搜索和查找的算法从不同的角度划分其实有多种分类。这里主要从两种角度来划分:
- 从搜索查找思想上划分
这种角度来划分搜索和查找有两大类:
暴力遍历:这是最容易理解的一种,从头到尾遍历集合中的每个对象找到符合条件的元素对象
自适应搜索:利用数据自身的特点(典型的就是排好序的数据集合)来通过算法优化搜索,它可能会引入额外的空间需求和数据预处理。搜索效率相对高 - 从数据类型分
如果从数据类型分可分为三类:
数据查找:一般是指对线性表、数组、树及哈希表等数据结构中查找元素对象
图或状态空间查找:主要是指在图、树或路径等中查找节点或最优解
字符串查找:在文本中查找指定的子字符串或符合条件的模式串
从宏观上掌握搜索和查找是做什么的?面对的目标和实现的结果是什么?就会有的放矢的去确定如何使用正确的搜索和查找算法。
三、算法分析
一般来说,常见的搜索和查找的算法主要有以下几种:
-
顺序查找
顺序查找也叫线性查找,它很简单,就是从头到尾逐个进行比较,直到找到指定目标或遍历完成即可。它的时间复杂度最好是O(1),最坏是O(n),平衡是O(n),其空间复杂度是O(1)。
顺序查找是一种最直观、简单的查找方式,它的适用性最强,它对查找的数据没有要求。但其线性增长的时间复杂度,并不适合海量数据的处理 -
二分(分块)查找
二分是指把查找集分成两块,但它要求数据是排序之后的。它是一种高效的搜索查找算法,通过不断的将查找数据空间减半来确定查找目标。其方式一般是比较搜索集中间元素对象与目标值,匹配则找到返回;否则,目标值小于中间元素,则在大于一侧的半集合内继续搜索;目标值大于中间元素,则在小于侧的半集合内继续搜索。如此迭代并最终找到目标或搜索结束。
分块是一种相对二分更宽松一些的查找,它在块间要求排序,但块内不要求。它和分块查找其实都属于分治法的范畴。它在块间可按类似二分的机制进行块的定位,在块内可以使用顺序查找的方式(如果已排序可继续使用分块或二分查找)。
二分查找的时间复杂度是O(log n),空间复杂度是O(1)。二分查找主要适应于排序好的数据集,所以它需要数据预处理,而排序往往也是一个很耗费时间或空间的过程。但二分查找对于大数据搜索有着很大的优势 -
哈希查找
哈希从某种角度看也是一种分块查找,只是它将块分得更细。作为一种快速查找技术,利用哈希函数将Key转换为索引并可以通过索引直接访问数据Value。它的查找速度与数据量大小无关,所以查询效率相当高。
它一般要先确定一个哈希函数,计算相关Key的哈希地址;然后确定该地址是否存在冲突(是否已有数据);如果没有直接存储,如果有,则使用链表、再哈希等方式解决冲突。
理论上来讲,哈希表的时间复杂度为O(1),但如果存在着较多的冲突,则可能有所变化,最坏可能为O(n);其空间复杂度为O(n)。哈希表的在海量数据搜索和查找中应用非常广泛,比如有名的布隆过滤器和布谷鸟过滤器。但使用哈希表有一定的要求,需要处理好哈希函数的选择,否则可能会达不到预定的设想 -
树查找
一般是指二叉搜索树BST,这种树有一定的要求即左子树的节点值都小于根根节点值,而右子树节点值都大于根节点值。查找时会根据当前的节点值与目标值比较来判断进入左子树或右子树。它和二分法一样同属于分治法的一类。
其具体的查找过程是从根节点开始匹配,比较目标值与当前节点值,成功则返回;如果目标值大则进入右子树,否则进入左子树。如此迭代,直到查找到指定的目标或搜索完成。
树查找的时间复杂度为O(log n),最坏则退化为链表为O(n);其空间复杂度为O(1)。树查找适合于一些复杂的情况和数据量大的情况,如数据库就是树的典型应用。由于树本身的操作和管理也相对复杂,所以一般对开发者的要求相对较高 -
深度优先查找
DFS,按俗话来说就是“一根筋”,它会沿着一条路径尽可能的深入,直到末尾才会进行回溯。可以简单理解为,遍历完成一“列”,再遍历另外一“列”,直到全部遍历完成(比如探索一栋楼,按单元搜索)。它一般借助栈来实现通过递归的方式,实现对数据集合的遍历
其时间复杂度为O(V + E),V为顶点数,E是边数。空间复杂度为O(V)。它和广度优先查找一般适合于图、树或迷宫等问题的解决 -
广度优先查找
BFS,从起始节点开始,先访问距离最近(距离为1)的节点,依次再访问次之(距离为2,3…)的节点。可以理解为按“行”或“层”来遍历,直到遍历完成所有集合数据(比如探索一栋楼,按层搜索)。它一般是使用队列来进行实现。
其时间复杂度为O(V + E),V为顶点数,E是边数。空间复杂度为O(V) -
字符串查找
Knuth-Morris-Pratt,其原理在于利用已经匹配过的部分信息,避免主串指针的回退。KMP通过预处理模式串,构建一个前缀函数的部分匹配表,通过匹配的结果来动态确定整体的滑动位数和不可能匹配成功的区间,同时保持主串指针的位置不变。
KMP专注在一个长文本(主串)中定位一个特定的子串(模式串)。它的优点是将字符串匹配的时间复杂度从“暴力回溯”的 O(n*m) 降到了理论最优的线性级别。
其时间复杂度为O(n + m),其中 n 是主串长度,m 是模式串长度。空间复杂度为O(m)。它一般适合于长文本中字符串的匹配防止进行全域的遍历 -
A查找
A-star Search,这种查找属于一种启发式搜索。它会利用估价函数来智能的决定先搜索哪个节点。其核心公式为:F(n) = G(n) + H(n)
G(n):起点到当前节点n的实际代价(已经走过的路)
H(n):从当前节点n到目标节点的估计代价(预估还要走的路,如曼哈顿距离、欧几里得距离)
F(n):综合评价。A在每一步都优先探索F(n)值最小的节点(使用优先队列)
其时间复杂度最坏为 O(b^d),b 是分支因子,d 是目标深度。空间复杂度为O(b^d)。其要求当H(n)满足可采纳性(Admissible)(即估计值 ≤ 实际值)时,A*找到的第一条路径必定是全局最优最短路径。
这种算法一般接触的比较少,主要应用于游戏和路径规则以及地图搜索中
其实算法是死的,但应用是灵活的,这就看解决问题的设计者的思想水平了。
四、应用
正如前面分析,哪种搜索和查找的算法,都各自有各自的优势,有时候甚至一种算法达不到目的。就可以使用多搜索算法结合,逐层分块推进来解决问题。下面给出一些常见的选择的建议:
- 数据规模不大且未排序,数据可能不断更新,查找特定目标,可考虑顺序查找
- 数据规模较大但已排序,数据无更新,查找特定目标,可考虑使用二分查找
- 如果对空间不太敏感,查询速度要求很高且数据无序,查找特定目标,可考虑哈希查找
- 如果数据空间很大,需要不断的动态维护(如数据库)和范围查询,可考虑使用二叉搜索树(如AVL或红黑树等)
- 如果需要对路径、状态或图进行处理,可考虑使用DFS、BFS、A*查找
- 对于字符串查找,一般建议使用KMP查找方式
要善于利于工程组合进行创新,不要拘泥于某个算法僵化、教条的使用它们。目的只有一个,高效、快捷的搜索和查找到指定目标。
五、例程
下面给出相对应的例程:
//顺序表查找
#include <iostream>
#include <vector>
int linearSearch(const std::vector<int>& arr, int target) {
for (int i = 0; i < static_cast<int>(arr.size()); ++i) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
int main() {
std::vector<int> arr = {6, 3, 9, 0, 7};
int index = linearSearch(arr, 9);
return 0;
}
//二分查找
#include <iostream>
#include <vector>
int binarySearch(const std::vector<int>& arr, int target) {
int left = 0;
int right = static_cast<int>(arr.size()) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
}
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
int main() {
std::vector<int> arr = {3, 12, 5, 6, 11};
std::cout << binarySearch(arr, 6) << std::endl;
return 0;
}
//哈希查找可参看STL中的std::unordered_map的find函数
//二叉搜索树
#include <iostream>
struct Node {
int value;
Node* left;
Node* right;
explicit Node(int v) : value(v), left(nullptr), right(nullptr) {}
};
Node* insert(Node* root, int value) {
if (root == nullptr) {
return new Node(value);
}
if (value < root->value) {
root->left = insert(root->left, value);
} else if (value > root->value) {
root->right = insert(root->right, value);
}
return root;
}
bool search(Node* root, int target) {
if (root == nullptr) {
return false;
}
if (root->value == target) {
return true;
}
if (target < root->value) {
return search(root->left, target);
}
return search(root->right, target);
}
int main() {
Node* root = nullptr;
root = insert(root, 9);
root = insert(root, 3);
root = insert(root, 10);
root = insert(root, 2);
root = insert(root, 6);
std::cout << std::boolalpha << search(root, 3) << std::endl;
std::cout << std::boolalpha << search(root, 10) << std::endl;
return 0;
}
//DFS,深度优先
#include <iostream>
#include <vector>
void dfs(int u, const std::vector<std::vector<int>>& graph, std::vector<bool>& visited) {
visited[u] = true;
for (int v : graph[u]) {
if (!visited[v]) {
dfs(v, graph, visited);
}
}
}
int main() {
std::vector<std::vector<int>> graph = {
{1, 2},{0, 3, 4},{0},{1},{1}
};
std::vector<bool> visited(graph.size(), false);
dfs(0, graph, visited);
return 0;
}
//KMP
#include <iostream>
#include <string>
#include <vector>
std::vector<int> buildNext(const std::string& pattern) {
std::vector<int> next(pattern.size(), 0);
int j = 0;
for (int i = 1; i < static_cast<int>(pattern.size()); ++i) {
while (j > 0 && pattern[i] != pattern[j]) {
j = next[j - 1];
}
if (pattern[i] == pattern[j]) {
++j;
}
next[i] = j;
}
return next;
}
int kmpSearch(const std::string& text, const std::string& pattern) {
if (pattern.empty()) {
return 0;
}
std::vector<int> next = buildNext(pattern);
int j = 0;
for (int i = 0; i < static_cast<int>(text.size()); ++i) {
while (j > 0 && text[i] != pattern[j]) {
j = next[j - 1];
}
if (text[i] == pattern[j]) {
++j;
}
if (j == static_cast<int>(pattern.size())) {
return i - j + 1;
}
}
return -1;
}
int main() {
std::string text = "aababcabcacbabd";
std::string pattern = "abcac";
std::cout << kmpSearch(text, pattern) << std::endl;
return 0;
}
//A*搜索
#include <cmath>
#include <iostream>
#include <queue>
#include <tuple>
#include <vector>
struct Node {
int x;
int y;
int g;
int f;
bool operator>(const Node& other) const {
return f > other.f;
}
};
int heuristic(int x1, int y1, int x2, int y2) {
return std::abs(x1 - x2) + std::abs(y1 - y2);
}
bool inBounds(int x, int y, int rows, int cols) {
return x >= 0 && x < rows && y >= 0 && y < cols;
}
int aStar(const std::vector<std::vector<int>>& grid,
int sx, int sy,
int tx, int ty) {
int rows = grid.size();
int cols = grid[0].size();
std::vector<std::vector<int>> dist(rows, std::vector<int>(cols, 1e9));
std::priority_queue<Node, std::vector<Node>, std::greater<Node>> pq;
dist[sx][sy] = 0;
pq.push({sx, sy, 0, heuristic(sx, sy, tx, ty)});
std::vector<std::pair<int, int>> dirs = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
while (!pq.empty()) {
Node cur = pq.top();
pq.pop();
if (cur.x == tx && cur.y == ty) {
return cur.g;
}
for (auto [dx, dy] : dirs) {
int nx = cur.x + dx;
int ny = cur.y + dy;
if (!inBounds(nx, ny, rows, cols)) {
continue;
}
if (grid[nx][ny] == 1) {
continue;
}
int newG = cur.g + 1;
if (newG < dist[nx][ny]) {
dist[nx][ny] = newG;
int newF = newG + heuristic(nx, ny, tx, ty);
pq.push({nx, ny, newG, newF});
}
}
}
return -1;
}
int main() {
std::vector<std::vector<int>> grid = {
{0, 0, 0, 0},
{1, 1, 0, 1},
{0, 0, 0, 0},
{0, 1, 1, 0}
};
int steps = aStar(grid, 0, 0, 3, 3);
std::cout << "shortest steps = " << steps << std::endl;
return 0;
}
广度遍历和深度遍历类似就不再给出相应的代码。
六、总结
搜索和查找是互联网和工程开发中最常见的一种应用。它的应用场景多种多样,数据大小、类别等也是各有不同。这就需要开发者在应用时要把握住特点和应用限制,实现设计的目标。不过对于大多数的应用来说,数据量都不大,所以更多的情况是开发者使用各种库提供的基础搜索和遍历都能达到目标。

1055

被折叠的 条评论
为什么被折叠?



