1 算法题 :使用哈希查找算法在无序数组中查找指定元素
1.1 题目含义
这个题目要求在一个无序数组中,使用哈希查找算法来查找指定的元素。在无序数组中查找元素,通常的线性查找方法的时间复杂度是 O(n),其中 n 是数组的长度。为了优化查找效率,可以使用哈希查找算法。
哈希查找算法的基本思想是通过一个哈希函数将数组中的每个元素映射到一个哈希表中的唯一位置。这样,当需要查找某个元素时,只需计算该元素的哈希值,然后直接在哈希表中查找对应的位置即可。如果哈希函数设计得当,且哈希表的大小足够大,那么哈希查找的时间复杂度可以接近 O(1)。
需要注意的是,哈希查找算法适用于在哈希表中进行查找,而不是直接在无序数组中查找。因此,在实际应用中,需要先构建一个哈希表,将无序数组中的元素作为键(key)存入哈希表,并将元素的索引或值作为对应的值(value)存储。然后,可以通过计算待查找元素的哈希值,在哈希表中查找该元素。
1.2 示例
示例 1:
输入:
- nums = [15, 3, 8, 4, 2]
- target = 3
输出:
- 1
说明:
- 因为元素 3 在数组中的索引是 1。
示例 2:
输入:
- nums = [10, 20, 30, 40, 50]
- target = 60
输出:
- -1
说明:
- 60 不存在于 nums 中,返回 -1。
示例 3:
输入:
- nums = []
- target = 0
输出:
- -1
说明:
- nums 为空,0 不存在于 nums 中,返回 -1。
2 解题思路
(1)准备阶段
- 确定哈希函数:
选择一个合适的哈希函数,该函数能够将数组中的每个元素映射到一个唯一的哈希值。哈希函数的选择对于减少哈希冲突至关重要。
常见的哈希函数包括直接取余法、平方取中法、折叠法等,具体选择哪种方法取决于元素的类型和分布。
- 确定哈希表大小:
根据数组的大小和预期的元素分布情况,确定一个合适的哈希表大小。哈希表的大小应该足够大,以减少哈希冲突的可能性。
哈希表的大小通常是质数,这有助于减少哈希冲突。
(2)构建哈希表
- 初始化哈希表:
创建一个空的哈希表,用于存储数组元素的哈希值和对应的索引。
- 遍历数组:
遍历无序数组中的每个元素。
对于每个元素,使用哈希函数计算其哈希值。
- 处理哈希冲突:
如果计算出的哈希值对应的哈希表位置已经被占用(即发生了哈希冲突),需要采用一种策略来解决冲突。
常见的解决哈希冲突的方法有链地址法(将具有相同哈希值的元素存储在一个链表中)和开放寻址法(通过一定的探测序列在哈希表中寻找空闲位置)。
在本题中,可以采用链地址法,即当哈希冲突发生时,在哈希表对应位置存储一个链表,链表中的节点包含元素和其在数组中的索引。
- 存储元素和索引:
将元素和其在数组中的索引存储到哈希表中对应的位置。如果发生哈希冲突,则按照解决冲突的策略进行处理。
(3)查找元素
- 计算待查找元素的哈希值:
使用相同的哈希函数计算待查找元素的哈希值。
- 在哈希表中查找:
根据计算出的哈希值,在哈希表中查找对应的元素。
如果哈希表对应位置没有元素,或者元素不匹配,说明待查找元素不在数组中。
如果找到了匹配的元素,则获取该元素在数组中的索引。
- 处理哈希冲突:
如果在查找过程中遇到了哈希冲突(即哈希表对应位置是一个链表),需要遍历链表来查找匹配的元素。
(4)返回结果
- 如果找到了匹配的元素,返回该元素在数组中的索引。
- 如果未找到匹配的元素,返回表示元素不在数组中的信息。
需要注意的是,哈希查找算法的时间复杂度主要取决于哈希函数的质量和哈希表的大小。一个理想的哈希函数能够将不同的元素映射到哈希表中的不同位置,从而减少哈希冲突的发生。同时,哈希表的大小也应该根据数组的大小和元素的分布情况来合理确定,以平衡查找效率和空间开销。
3 算法实现代码
3.1 自定义哈希函数与解决哈希冲突
如下为算法实现代码:
#include <iostream>
#include <vector>
#include <list>
#include <cmath>
using namespace std;
// 哈希函数示例,使用取余法
int hashFunction(int key, int tableSize) {
return key % tableSize;
}
// 哈希表节点,存储元素值和其在数组中的索引
struct HashTableNode {
int value;
int index;
HashTableNode(int v, int i) : value(v), index(i) {}
};
class Solution
{
public:
// 查找函数,返回元素在数组中的索引,如果未找到则返回-1
int findElementPosition(const vector<int>& arr, int target) {
const int tableSize = 10; // 哈希表大小,可以根据实际情况调整
vector<list<HashTableNode>> hashTable(tableSize); // 哈希表,每个位置是一个链表
// 构建哈希表
for (size_t i = 0; i < arr.size(); ++i) {
int hashIndex = hashFunction(arr[i], tableSize); // 计算哈希值
hashTable[hashIndex].push_back(HashTableNode(arr[i], i)); // 存储元素和索引
}
// 查找元素
int hashIndex = hashFunction(target, tableSize); // 计算待查找元素的哈希值
for (const auto& node : hashTable[hashIndex]) {
if (node.value == target) {
return node.index; // 找到匹配的元素,返回其在数组中的索引
}
}
// 返回结果
return -1; // 未找到匹配的元素
}
};
这个实现定义了一个简单的哈希函数 hashFunction,它使用取余法将元素值映射到哈希表的索引。HashTableNode 结构体用于存储元素值及其在数组中的索引。哈希表 hashTable 是一个 vector,它的每个元素都是一个 list,用于处理哈希冲突。
在构建哈希表时,遍历数组中的每个元素,计算其哈希值,并将元素和索引作为 HashTableNode 对象存储到对应的链表中。
在查找元素时,首先计算待查找元素的哈希值,然后遍历对应链表中的节点,查找匹配的元素。如果找到了匹配的元素,就返回其在数组中的索引;否则,返回 -1 表示元素不在数组中。
调用上面的算法,并得到输出:
int main()
{
Solution s;
vector<int> arr = { 5, 15, 25, 35, 45, 10, 20, 30, 40, 50 }; // 无序数组
int target = 35; // 待查找的元素
int position = s.findElementPosition(arr, target);
if (position != -1) {
std::cout << "Element found at index: " << position << std::endl;
}
else {
std::cout << "Element not found in the array." << std::endl;
}
return 0;
}
上面代码的输出为:
Element found at index: 3
3.2 使用 std::unordered_map
std::unordered_map 是C++标准库提供的一个高效的哈希表实现,处理本算法题,更为方便。
如下为算法实现代码:
#include <iostream>
#include <unordered_map>
using namespace std;
class Solution
{
public:
// 查找函数,返回元素在数组中的索引,如果未找到则返回-1
int findElementPosition(const vector<int>& arr, int target) {
// 构建哈希表
unordered_map<int, int> hashTable; // 键为数组元素,值为元素在数组中的索引
for (size_t i = 0; i < arr.size(); ++i) {
hashTable[arr[i]] = i; // 存储元素和索引
}
// 查找元素
// 计算待查找元素的哈希值(这一步由unordered_map自动完成)
auto it = hashTable.find(target); // 在哈希表中查找
// 返回结果
if (it != hashTable.end()) {
// 如果找到了匹配的元素,返回该元素在数组中的索引
return it->second;
}
else {
// 如果未找到匹配的元素,返回表示元素不在数组中的信息
return -1;
}
}
};
这个示例使用了 std::unordered_map 来作为哈希表,它内部已经实现了哈希函数和冲突解决策略(通常是链地址法)。通过遍历数组,将元素作为键,将元素的索引作为值存储到哈希表中。然后,使用 find 方法在哈希表中查找目标元素。如果找到了,就返回对应的索引;否则,返回 -1 表示元素不在数组中。
4 测试用例
以下是针对上面算法的测试用例,基本覆盖了各种情况:
(1)基础测试用例
输入:数组 [3, 5, -1, 0, 9, 12],目标值 9
输出:4
说明:目标值 9 存在于数组中,位于索引 4 的位置。
(2)目标值不存在于数组中
输入:数组 [3, 5, -1, 0, 9, 12],目标值 2
输出:-1
说明:目标值 2 不存在于数组中。
(3)目标值位于数组开头
输入:数组 [-1, 0, 3, 9, 5, 12],目标值 -1
输出:0
说明:目标值 -1 位于数组的开头,即索引 0 的位置。
(4)目标值位于数组末尾
输入:数组 [9, -1, 3, 0, 5, 12],目标值 12
输出:5
说明:目标值 12 位于数组的末尾,即索引 5 的位置。
(5)目标值位于数组中间
输入:数组 [0, -1, 3, 9, 5, 12],目标值 3
输出:2
说明:目标值 3 位于数组的中间位置,即索引 2 的位置。
(6)空数组
输入:数组 [],目标值 9
输出:-1
说明:空数组不包含任何元素,因此无法找到目标值。
(7)数组只有一个元素
输入:数组 [9],目标值 9
输出:0
说明:数组只有一个元素,且该元素就是目标值,位于索引 0 的位置。
(8)数组中存在多个相同的目标值
输入:数组 [1, 2, 3, 3, 4, 5],目标值 3
输出:2 或 3
说明:数组中存在多个目标值 3,返回任意一个目标值的索引都是正确的。这里可以返回 2 或 3。